From 76346cbddfd5fa8bccad12193e915c00c8873491 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 13 Apr 2018 16:10:22 -0700 Subject: [PATCH 001/456] Implementing support for APNS content-mutable field (#156) * Implementing support for APNS content-mutable field * Updated annotations and docs * Corrected mutable-content option name * Renamed customFields to customData * Deleted test Main --- CHANGELOG.md | 2 + .../google/firebase/messaging/ApnsConfig.java | 2 +- .../com/google/firebase/messaging/Aps.java | 105 +++++++++++++----- .../firebase/messaging/MessageTest.java | 20 +++- 4 files changed, 99 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 689833459..e0800756c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- [added] Added new `setMutableContent()`, `putCustomData()` and + `putAllCustomData()` methods to the `Aps.Builder` API. - [fixed] Improved error handling in FCM by mapping more server-side errors to client-side error codes. diff --git a/src/main/java/com/google/firebase/messaging/ApnsConfig.java b/src/main/java/com/google/firebase/messaging/ApnsConfig.java index cc486c2c6..5e503c0ea 100644 --- a/src/main/java/com/google/firebase/messaging/ApnsConfig.java +++ b/src/main/java/com/google/firebase/messaging/ApnsConfig.java @@ -45,7 +45,7 @@ private ApnsConfig(Builder builder) { this.headers = builder.headers.isEmpty() ? null : ImmutableMap.copyOf(builder.headers); this.payload = ImmutableMap.builder() .putAll(builder.customData) - .put("aps", builder.aps) + .put("aps", builder.aps.getFields()) .build(); } diff --git a/src/main/java/com/google/firebase/messaging/Aps.java b/src/main/java/com/google/firebase/messaging/Aps.java index c35d550d4..52922c741 100644 --- a/src/main/java/com/google/firebase/messaging/Aps.java +++ b/src/main/java/com/google/firebase/messaging/Aps.java @@ -18,8 +18,11 @@ 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.ImmutableMap; +import com.google.firebase.internal.NonNull; +import java.util.HashMap; +import java.util.Map; /** * Represents the @@ -27,37 +30,41 @@ */ public class Aps { - @Key("alert") - private final Object alert; - - @Key("badge") - private final Integer badge; - - @Key("sound") - private final String sound; - - @Key("content-available") - private final Integer contentAvailable; - - @Key("category") - private final String category; - - @Key("thread-id") - private final String threadId; + private final Map fields; private Aps(Builder builder) { checkArgument(Strings.isNullOrEmpty(builder.alertString) || (builder.alert == null), "Multiple alert specifications (string and ApsAlert) found."); + ImmutableMap.Builder fields = ImmutableMap.builder(); if (builder.alert != null) { - this.alert = builder.alert; - } else { - this.alert = builder.alertString; - } - this.badge = builder.badge; - this.sound = builder.sound; - this.contentAvailable = builder.contentAvailable ? 1 : null; - this.category = builder.category; - this.threadId = builder.threadId; + fields.put("alert", builder.alert); + } else if (builder.alertString != null) { + fields.put("alert", builder.alertString); + } + if (builder.badge != null) { + fields.put("badge", builder.badge); + } + if (builder.sound != null) { + fields.put("sound", builder.sound); + } + if (builder.contentAvailable) { + fields.put("content-available", 1); + } + if (builder.mutableContent) { + fields.put("mutable-content", 1); + } + if (builder.category != null) { + fields.put("category", builder.category); + } + if (builder.threadId != null) { + fields.put("thread-id", builder.threadId); + } + fields.putAll(builder.customData); + this.fields = fields.build(); + } + + Map getFields() { + return this.fields; } /** @@ -76,8 +83,10 @@ public static class Builder { private Integer badge; private String sound; private boolean contentAvailable; + private boolean mutableContent; private String category; private String threadId; + private final Map customData = new HashMap<>(); private Builder() {} @@ -137,6 +146,41 @@ public Builder setContentAvailable(boolean contentAvailable) { return this; } + /** + * Specifies whether to set the {@code mutable-content} property on the message, so the + * clients can modify the notification via app extensions. + * + * @param mutableContent True to make the content mutable via app extensions. + * @return This builder. + */ + public Builder setMutableContent(boolean mutableContent) { + this.mutableContent = mutableContent; + return this; + } + + /** + * Puts a custom key-value pair to the aps dictionary. + * + * @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 aps dictionary. + * + * @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; + } + /** * Sets the notification type. * @@ -159,6 +203,13 @@ public Builder setThreadId(String threadId) { return this; } + /** + * Builds a new {@link Aps} instance from the fields set on this builder. + * + * @return A non-null {@link Aps}. + * @throws IllegalArgumentException If the alert is specified both as an object and a string. + * Or if the same field is set both using a setter method, and as a custom field. + */ public Aps build() { return new Aps(this); } diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 21caeeb18..3313f2bb3 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -36,7 +36,7 @@ public class MessageTest { @Test(expected = IllegalArgumentException.class) - public void testMessageWithoutTarget() throws IOException { + public void testMessageWithoutTarget() { Message.builder().build(); } @@ -214,7 +214,7 @@ public void testAndroidMessageWithoutLocalization() throws IOException { } @Test - public void testInvalidAndroidConfig() throws IOException { + public void testInvalidAndroidConfig() { try { AndroidConfig.builder().setTtl(-1).build(); fail("No error thrown for invalid ttl"); @@ -363,6 +363,7 @@ public void testApnsMessageWithPayloadAndAps() throws IOException { .setBadge(42) .setCategory("test-category") .setContentAvailable(true) + .setMutableContent(true) .setSound("test-sound") .setThreadId("test-thread-id") .build()) @@ -376,6 +377,7 @@ public void testApnsMessageWithPayloadAndAps() throws IOException { .put("badge", new BigDecimal(42)) .put("category", "test-category") .put("content-available", new BigDecimal(1)) + .put("mutable-content", new BigDecimal(1)) .put("sound", "test-sound") .put("thread-id", "test-thread-id") .build()); @@ -404,6 +406,8 @@ public void testApnsMessageWithPayloadAndAps() throws IOException { .setCategory("test-category") .setSound("test-sound") .setThreadId("test-thread-id") + .putCustomData("ck1", "cv1") + .putAllCustomData(ImmutableMap.of("ck2", "cv2", "ck3", 1)) .build()) .build()) .setTopic("test-topic") @@ -424,6 +428,9 @@ public void testApnsMessageWithPayloadAndAps() throws IOException { .put("category", "test-category") .put("sound", "test-sound") .put("thread-id", "test-thread-id") + .put("ck1", "cv1") + .put("ck2", "cv2") + .put("ck3", new BigDecimal(1)) .build()); assertJsonEquals( ImmutableMap.of( @@ -456,6 +463,15 @@ public void testInvalidApnsConfig() { // 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 notificationBuilders = ImmutableList.of( ApsAlert.builder().addLocalizationArg("foo"), ApsAlert.builder().addTitleLocalizationArg("foo") From 4a45659ea1e6906c1a493d6492aa1eff34653a96 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 13 Apr 2018 17:25:02 -0700 Subject: [PATCH 002/456] Support configuring timeout for outbound requests (#158) * Support configuring timeout for outbound requests * Documentation updates * Updated changelog * Updated docs --- CHANGELOG.md | 3 + .../com/google/firebase/FirebaseOptions.java | 57 +++++++++++++++ .../google/firebase/auth/FirebaseAuth.java | 3 +- .../firebase/auth/FirebaseUserManager.java | 17 ++--- .../firebase/iid/FirebaseInstanceId.java | 7 +- .../internal/FirebaseRequestInitializer.java | 50 +++++++++++++ .../firebase/messaging/FirebaseMessaging.java | 7 +- .../google/firebase/FirebaseOptionsTest.java | 24 +++++++ .../auth/FirebaseUserManagerTest.java | 22 ++++++ .../FirebaseRequestInitializerTest.java | 71 +++++++++++++++++++ 10 files changed, 241 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/google/firebase/internal/FirebaseRequestInitializer.java create mode 100644 src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e0800756c..2c665ecdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased +- [added] Connection timeout and read timeout for HTTP/REST connections + can now be configured via `FirebaseOptions.Builder` at app + initialization. - [added] Added new `setMutableContent()`, `putCustomData()` and `putAllCustomData()` methods to the `Aps.Builder` API. - [fixed] Improved error handling in FCM by mapping more server-side diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java index fc4df2658..28c9e9e66 100644 --- a/src/main/java/com/google/firebase/FirebaseOptions.java +++ b/src/main/java/com/google/firebase/FirebaseOptions.java @@ -47,6 +47,8 @@ public final class FirebaseOptions { private final Map databaseAuthVariableOverride; private final String projectId; private final HttpTransport httpTransport; + private final int connectTimeout; + private final int readTimeout; private final JsonFactory jsonFactory; private final ThreadManager threadManager; @@ -68,6 +70,10 @@ private FirebaseOptions(@NonNull FirebaseOptions.Builder builder) { "FirebaseOptions must be initialized with a non-null JsonFactory."); this.threadManager = checkNotNull(builder.threadManager, "FirebaseOptions must be initialized with a non-null ThreadManager."); + checkArgument(builder.connectTimeout >= 0); + this.connectTimeout = builder.connectTimeout; + checkArgument(builder.readTimeout >= 0); + this.readTimeout = builder.readTimeout; } /** @@ -132,6 +138,26 @@ public JsonFactory getJsonFactory() { return jsonFactory; } + /** + * Returns the connect timeout in milliseconds, which is applied to outgoing REST calls + * made by the SDK. + * + * @return Connect timeout in milliseconds. 0 indicates an infinite timeout. + */ + public int getConnectTimeout() { + return connectTimeout; + } + + /** + * Returns the read timeout in milliseconds, which is applied to outgoing REST calls + * made by the SDK. + * + * @return Read timeout in milliseconds. 0 indicates an infinite timeout. + */ + public int getReadTimeout() { + return readTimeout; + } + @NonNull ThreadManager getThreadManager() { return threadManager; @@ -157,6 +183,8 @@ public static final class Builder { private HttpTransport httpTransport = Utils.getDefaultTransport(); private JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); private ThreadManager threadManager = FirebaseThreadManagers.DEFAULT_THREAD_MANAGER; + private int connectTimeout; + private int readTimeout; /** Constructs an empty builder. */ public Builder() {} @@ -176,6 +204,8 @@ public Builder(FirebaseOptions options) { httpTransport = options.httpTransport; jsonFactory = options.jsonFactory; threadManager = options.threadManager; + connectTimeout = options.connectTimeout; + readTimeout = options.readTimeout; } /** @@ -321,6 +351,33 @@ public Builder setThreadManager(ThreadManager threadManager) { 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 + * affect the {@link com.google.firebase.database.FirebaseDatabase} and + * {@link com.google.firebase.cloud.FirestoreClient} APIs. + * + * @param connectTimeout Connect timeout in milliseconds. Must not be negative. + * @return This Builder instance is returned so subsequent calls can be chained. + */ + public Builder setConnectTimeout(int connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + /** + * Sets the read timeout for outgoing HTTP (REST) calls made by the SDK. This does not affect + * the {@link com.google.firebase.database.FirebaseDatabase} and + * {@link com.google.firebase.cloud.FirestoreClient} APIs. + * + * @param readTimeout Read timeout in milliseconds. Must not be negative. + * @return This Builder instance is returned so subsequent calls can be chained. + */ + public Builder setReadTimeout(int readTimeout) { + this.readTimeout = readTimeout; + return this; + } + /** * Builds the {@link FirebaseOptions} instance from the previously set options. * diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index bbb9feb2b..5f295c153 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -85,8 +85,7 @@ private FirebaseAuth(FirebaseApp firebaseApp) { this.credentials = ImplFirebaseTrampolines.getCredentials(firebaseApp); this.projectId = ImplFirebaseTrampolines.getProjectId(firebaseApp); this.jsonFactory = firebaseApp.getOptions().getJsonFactory(); - this.userManager = new FirebaseUserManager(jsonFactory, - firebaseApp.getOptions().getHttpTransport(), this.credentials); + this.userManager = new FirebaseUserManager(firebaseApp); this.destroyed = new AtomicBoolean(false); this.lock = new Object(); } diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 504476400..79090ed2c 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -30,18 +30,19 @@ import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonObjectParser; -import com.google.auth.http.HttpCredentialsAdapter; -import com.google.auth.oauth2.GoogleCredentials; 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.auth.UserRecord.CreateRequest; import com.google.firebase.auth.UserRecord.UpdateRequest; import com.google.firebase.auth.internal.DownloadAccountResponse; import com.google.firebase.auth.internal.GetAccountInfoResponse; import com.google.firebase.auth.internal.HttpErrorResponse; +import com.google.firebase.internal.FirebaseRequestInitializer; +import com.google.firebase.internal.NonNull; import com.google.firebase.internal.SdkUtils; import java.io.IOException; import java.util.List; @@ -98,13 +99,13 @@ class FirebaseUserManager { /** * Creates a new FirebaseUserManager instance. * - * @param jsonFactory JsonFactory instance used to transform Java objects into JSON and back. - * @param transport HttpTransport used to make REST API calls. + * @param app A non-null {@link FirebaseApp}. */ - FirebaseUserManager(JsonFactory jsonFactory, HttpTransport transport, - GoogleCredentials credentials) { - this.jsonFactory = checkNotNull(jsonFactory, "jsonFactory must not be null"); - this.requestFactory = transport.createRequestFactory(new HttpCredentialsAdapter(credentials)); + FirebaseUserManager(@NonNull FirebaseApp app) { + checkNotNull(app, "FirebaseApp must not be null"); + this.jsonFactory = app.getOptions().getJsonFactory(); + HttpTransport transport = app.getOptions().getHttpTransport(); + this.requestFactory = transport.createRequestFactory(new FirebaseRequestInitializer(app)); } @VisibleForTesting diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java index 6993b8fd1..9057905ca 100644 --- a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java +++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java @@ -28,14 +28,13 @@ import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonObjectParser; import com.google.api.core.ApiFuture; -import com.google.auth.http.HttpCredentialsAdapter; -import com.google.auth.oauth2.GoogleCredentials; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteStreams; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.internal.FirebaseRequestInitializer; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.TaskToApiFuture; @@ -74,10 +73,8 @@ public class FirebaseInstanceId { private FirebaseInstanceId(FirebaseApp app) { HttpTransport httpTransport = app.getOptions().getHttpTransport(); - GoogleCredentials credentials = ImplFirebaseTrampolines.getCredentials(app); this.app = app; - this.requestFactory = httpTransport.createRequestFactory( - new HttpCredentialsAdapter(credentials)); + this.requestFactory = httpTransport.createRequestFactory(new FirebaseRequestInitializer(app)); this.jsonFactory = app.getOptions().getJsonFactory(); this.projectId = ImplFirebaseTrampolines.getProjectId(app); checkArgument(!Strings.isNullOrEmpty(projectId), diff --git a/src/main/java/com/google/firebase/internal/FirebaseRequestInitializer.java b/src/main/java/com/google/firebase/internal/FirebaseRequestInitializer.java new file mode 100644 index 000000000..0cd4e9393 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/FirebaseRequestInitializer.java @@ -0,0 +1,50 @@ +/* + * 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.internal; + +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.auth.http.HttpCredentialsAdapter; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; +import java.io.IOException; + +/** + * {@code HttpRequestInitializer} for configuring outgoing REST calls. Handles OAuth2 authorization + * and setting timeout values. + */ +public class FirebaseRequestInitializer implements HttpRequestInitializer { + + private final HttpCredentialsAdapter credentialsAdapter; + private final int connectTimeout; + private final int readTimeout; + + public FirebaseRequestInitializer(FirebaseApp app) { + GoogleCredentials credentials = ImplFirebaseTrampolines.getCredentials(app); + this.credentialsAdapter = new HttpCredentialsAdapter(credentials); + this.connectTimeout = app.getOptions().getConnectTimeout(); + this.readTimeout = app.getOptions().getReadTimeout(); + } + + @Override + public void initialize(HttpRequest httpRequest) throws IOException { + credentialsAdapter.initialize(httpRequest); + httpRequest.setConnectTimeout(connectTimeout); + httpRequest.setReadTimeout(readTimeout); + } +} diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 21ab3898f..2daec8490 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -33,13 +33,12 @@ import com.google.api.client.json.JsonParser; import com.google.api.client.util.Key; import com.google.api.core.ApiFuture; -import com.google.auth.http.HttpCredentialsAdapter; -import com.google.auth.oauth2.GoogleCredentials; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.internal.FirebaseRequestInitializer; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.TaskToApiFuture; @@ -102,10 +101,8 @@ public class FirebaseMessaging { private FirebaseMessaging(FirebaseApp app) { HttpTransport httpTransport = app.getOptions().getHttpTransport(); - GoogleCredentials credentials = ImplFirebaseTrampolines.getCredentials(app); this.app = app; - this.requestFactory = httpTransport.createRequestFactory( - new HttpCredentialsAdapter(credentials)); + this.requestFactory = httpTransport.createRequestFactory(new FirebaseRequestInitializer(app)); this.jsonFactory = app.getOptions().getJsonFactory(); String projectId = ImplFirebaseTrampolines.getProjectId(app); checkArgument(!Strings.isNullOrEmpty(projectId), diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java index b02fbbdc9..33e07aeb5 100644 --- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java +++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java @@ -87,6 +87,8 @@ public void createOptionsWithAllValuesSet() throws IOException, InterruptedExcep .setJsonFactory(jsonFactory) .setHttpTransport(httpTransport) .setThreadManager(MOCK_THREAD_MANAGER) + .setConnectTimeout(30000) + .setReadTimeout(60000) .build(); assertEquals(FIREBASE_DB_URL, firebaseOptions.getDatabaseUrl()); assertEquals(FIREBASE_STORAGE_BUCKET, firebaseOptions.getStorageBucket()); @@ -94,6 +96,8 @@ public void createOptionsWithAllValuesSet() throws IOException, InterruptedExcep assertSame(jsonFactory, firebaseOptions.getJsonFactory()); assertSame(httpTransport, firebaseOptions.getHttpTransport()); assertSame(MOCK_THREAD_MANAGER, firebaseOptions.getThreadManager()); + assertEquals(30000, firebaseOptions.getConnectTimeout()); + assertEquals(60000, firebaseOptions.getReadTimeout()); GoogleCredentials credentials = firebaseOptions.getCredentials(); assertNotNull(credentials); @@ -114,6 +118,8 @@ public void createOptionsWithOnlyMandatoryValuesSet() throws IOException, Interr assertNotNull(firebaseOptions.getThreadManager()); assertNull(firebaseOptions.getDatabaseUrl()); assertNull(firebaseOptions.getStorageBucket()); + assertEquals(0, firebaseOptions.getConnectTimeout()); + assertEquals(0, firebaseOptions.getReadTimeout()); GoogleCredentials credentials = firebaseOptions.getCredentials(); assertNotNull(credentials); @@ -200,6 +206,24 @@ public void checkToBuilderCreatesNewEquivalentInstance() { assertEquals(ALL_VALUES_OPTIONS.getJsonFactory(), allValuesOptionsCopy.getJsonFactory()); assertEquals(ALL_VALUES_OPTIONS.getHttpTransport(), allValuesOptionsCopy.getHttpTransport()); assertEquals(ALL_VALUES_OPTIONS.getThreadManager(), allValuesOptionsCopy.getThreadManager()); + assertEquals(ALL_VALUES_OPTIONS.getConnectTimeout(), allValuesOptionsCopy.getConnectTimeout()); + assertEquals(ALL_VALUES_OPTIONS.getReadTimeout(), allValuesOptionsCopy.getReadTimeout()); + } + + @Test(expected = IllegalArgumentException.class) + public void createOptionsWithInvalidConnectTimeout() { + new FirebaseOptions.Builder() + .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) + .setConnectTimeout(-1) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void createOptionsWithInvalidReadTimeout() { + new FirebaseOptions.Builder() + .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) + .setReadTimeout(-1) + .build(); } @Test diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index de6862bcc..3f29b06a3 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -25,6 +25,7 @@ import com.google.api.client.googleapis.util.Utils; import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponseException; import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; @@ -382,6 +383,27 @@ public void testGetUserUnexpectedHttpError() throws Exception { } } + @Test + public void testTimeout() throws Exception { + MockHttpTransport transport = new MultiRequestMockHttpTransport(ImmutableList.of( + new MockLowLevelHttpResponse().setContent(TestUtils.loadResource("getUser.json")))); + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport(transport) + .setConnectTimeout(30000) + .setReadTimeout(60000) + .build()); + FirebaseAuth auth = FirebaseAuth.getInstance(); + FirebaseUserManager userManager = auth.getUserManager(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + userManager.setInterceptor(interceptor); + + FirebaseAuth.getInstance().getUserAsync("testuser").get(); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(30000, request.getConnectTimeout()); + assertEquals(60000, request.getReadTimeout()); + } + @Test public void testUserBuilder() { Map map = new CreateRequest().getProperties(); diff --git a/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java b/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java new file mode 100644 index 000000000..c7919eb97 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java @@ -0,0 +1,71 @@ +/* + * 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.internal; + +import static org.junit.Assert.assertEquals; + +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.HttpTransport; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.MockGoogleCredentials; +import org.junit.After; +import org.junit.Test; + +public class FirebaseRequestInitializerTest { + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testDefaultTimeouts() throws Exception { + FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("token")) + .build()); + HttpTransport transport = new MockHttpTransport(); + HttpRequestFactory factory = transport.createRequestFactory( + new FirebaseRequestInitializer(app)); + HttpRequest request = factory.buildGetRequest( + new GenericUrl("https://firebase.google.com")); + assertEquals(0, request.getConnectTimeout()); + assertEquals(0, request.getReadTimeout()); + assertEquals("Bearer token", request.getHeaders().getAuthorization()); + } + + @Test + public void testExplicitTimeouts() throws Exception { + FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("token")) + .setConnectTimeout(30000) + .setReadTimeout(60000) + .build()); + HttpTransport transport = new MockHttpTransport(); + HttpRequestFactory factory = transport.createRequestFactory( + new FirebaseRequestInitializer(app)); + HttpRequest request = factory.buildGetRequest( + new GenericUrl("https://firebase.google.com")); + assertEquals(30000, request.getConnectTimeout()); + assertEquals(60000, request.getReadTimeout()); + assertEquals("Bearer token", request.getHeaders().getAuthorization()); + } +} From cafabe62ba62189988f35e4d67ebbafe9415ca95 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 16 Apr 2018 14:30:07 -0700 Subject: [PATCH 003/456] Minor doc update (#163) --- src/main/java/com/google/firebase/messaging/Aps.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/Aps.java b/src/main/java/com/google/firebase/messaging/Aps.java index 52922c741..17912d1e3 100644 --- a/src/main/java/com/google/firebase/messaging/Aps.java +++ b/src/main/java/com/google/firebase/messaging/Aps.java @@ -147,8 +147,8 @@ public Builder setContentAvailable(boolean contentAvailable) { } /** - * Specifies whether to set the {@code mutable-content} property on the message, so the - * clients can modify the notification via app extensions. + * Specifies whether to set the {@code mutable-content} property on the message. When set, this + * property allows clients to modify the notification via app extensions. * * @param mutableContent True to make the content mutable via app extensions. * @return This builder. From 962126aa1ca3d9da8e28f7c387e614336854977c Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 17 Apr 2018 11:23:08 -0700 Subject: [PATCH 004/456] Staged Release 5.10.0 (#164) * Updating CHANGELOG for 5.10.0 release. * [maven-release-plugin] prepare release v5.10.0 * [maven-release-plugin] prepare for next development iteration --- CHANGELOG.md | 5 +++++ pom.xml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c665ecdc..a0fa64c95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Unreleased +- + +# v5.10.0 + + - [added] Connection timeout and read timeout for HTTP/REST connections can now be configured via `FirebaseOptions.Builder` at app initialization. diff --git a/pom.xml b/pom.xml index f1d07a8c7..807ff3bca 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 5.9.1-SNAPSHOT + 5.10.1-SNAPSHOT jar firebase-admin From a850e7f4e7e3425a8762c7d1e4a69dca957cdda5 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 20 Apr 2018 09:42:51 -0700 Subject: [PATCH 005/456] Upgrading Firestore, GCS and Netty to recent versions (#165) * Upgrading Firestore, GCS and Netty to recent versions * Updated changelog --- CHANGELOG.md | 4 +++- pom.xml | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0fa64c95..7e4583197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Unreleased -- +- [fixed] Upgraded Cloud Firestore dependency version to 0.44.0-beta. +- [fixed] Upgraded Cloud Storage dependency version to 1.26.0. +- [fixed] Upgraded Netty dependency version to 4.1.22. # v5.10.0 diff --git a/pom.xml b/pom.xml index 807ff3bca..c297759b3 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.17.Final + 4.1.22.Final @@ -406,12 +406,12 @@ com.google.cloud google-cloud-storage - 1.15.0 + 1.26.0 com.google.cloud google-cloud-firestore - 0.33.0-beta + 0.44.0-beta From 6baace6966f665c9b31b84158db69ff599a2ecbf Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Sat, 21 Apr 2018 11:18:06 -0700 Subject: [PATCH 006/456] Session Cookie Management API (#161) * Added createSessionCookie() method * Fixed lint error * Added cookie verification logic * Added snippets and updated changelog * Minor refactoring of internal APIs * Adding missing newline at eof * Fixing a verification check * Responding to code review comments; Using String.format() to construct error messages; Updated documentation and other readability improvements * Renamed helper method --- CHANGELOG.md | 9 +- pom.xml | 7 + .../google/firebase/auth/FirebaseAuth.java | 138 +++++++-- .../firebase/auth/FirebaseUserManager.java | 15 + .../firebase/auth/SessionCookieOptions.java | 76 +++++ .../auth/internal/FirebaseTokenFactory.java | 2 +- .../auth/internal/FirebaseTokenVerifier.java | 241 +++++++++++----- .../firebase/auth/internal/KeyManagers.java | 85 ++++++ .../google/firebase/auth/FirebaseAuthIT.java | 42 ++- .../auth/FirebaseUserManagerTest.java | 74 +++++ .../internal/FirebaseTokenVerifierTest.java | 270 ++++++++++++++---- .../snippets/FirebaseAuthSnippets.java | 145 +++++++++- src/test/resources/createSessionCookie.json | 3 + 13 files changed, 952 insertions(+), 155 deletions(-) create mode 100644 src/main/java/com/google/firebase/auth/SessionCookieOptions.java create mode 100644 src/main/java/com/google/firebase/auth/internal/KeyManagers.java create mode 100644 src/test/resources/createSessionCookie.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e4583197..352f2c06b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,19 @@ # Unreleased +- [added] A new `FirebaseAuth.createSessionCookieAsync()` method for + creating a long-lived session cookie given a valid ID token. +- [added] A new `FirebaseAuth.verifySessionCookieAsync()` method for + verifying a given cookie string is valid. - [fixed] Upgraded Cloud Firestore dependency version to 0.44.0-beta. - [fixed] Upgraded Cloud Storage dependency version to 1.26.0. - [fixed] Upgraded Netty dependency version to 4.1.22. # v5.10.0 - +- [fixed] Using the `HttpTransport` specified at `FirebaseOptions` in + `GooglePublicKeysManager`. This enables developers to use a custom + transport to fetch public keys when verifying ID tokens and session + cookies. - [added] Connection timeout and read timeout for HTTP/REST connections can now be configured via `FirebaseOptions.Builder` at app initialization. diff --git a/pom.xml b/pom.xml index c297759b3..395c62137 100644 --- a/pom.xml +++ b/pom.xml @@ -471,5 +471,12 @@ 4.12 test + + + javax.ws.rs + javax.ws.rs-api + 2.0 + test + diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 5f295c153..3150b1d75 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -20,7 +20,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; -import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; import com.google.api.client.json.JsonFactory; import com.google.api.client.util.Clock; import com.google.api.core.ApiFuture; @@ -36,7 +35,9 @@ import com.google.firebase.auth.UserRecord.UpdateRequest; import com.google.firebase.auth.internal.FirebaseTokenFactory; import com.google.firebase.auth.internal.FirebaseTokenVerifier; +import com.google.firebase.auth.internal.KeyManagers; import com.google.firebase.internal.FirebaseService; +import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; import com.google.firebase.internal.TaskToApiFuture; import com.google.firebase.tasks.Task; @@ -54,10 +55,10 @@ */ public class FirebaseAuth { - private final GooglePublicKeysManager googlePublicKeysManager; private final Clock clock; private final FirebaseApp firebaseApp; + private final KeyManagers keyManagers; private final GoogleCredentials credentials; private final String projectId; private final JsonFactory jsonFactory; @@ -66,10 +67,7 @@ public class FirebaseAuth { private final Object lock; private FirebaseAuth(FirebaseApp firebaseApp) { - this(firebaseApp, - FirebaseTokenVerifier.buildGooglePublicKeysManager( - firebaseApp.getOptions().getHttpTransport()), - Clock.SYSTEM); + this(firebaseApp, KeyManagers.getDefault(firebaseApp, Clock.SYSTEM), Clock.SYSTEM); } /** @@ -77,10 +75,9 @@ private FirebaseAuth(FirebaseApp firebaseApp) { * correctly signed. This should only be used for testing to override the default key manager. */ @VisibleForTesting - FirebaseAuth( - FirebaseApp firebaseApp, GooglePublicKeysManager googlePublicKeysManager, Clock clock) { + FirebaseAuth(FirebaseApp firebaseApp, KeyManagers keyManagers, Clock clock) { this.firebaseApp = checkNotNull(firebaseApp); - this.googlePublicKeysManager = checkNotNull(googlePublicKeysManager); + this.keyManagers = checkNotNull(keyManagers); this.clock = checkNotNull(clock); this.credentials = ImplFirebaseTrampolines.getCredentials(firebaseApp); this.projectId = ImplFirebaseTrampolines.getProjectId(firebaseApp); @@ -114,6 +111,106 @@ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { return service.getInstance(); } + private Task createSessionCookie( + final String idToken, final SessionCookieOptions options) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty"); + checkNotNull(options, "options must not be null"); + return call(new Callable() { + @Override + public String call() throws Exception { + return userManager.createSessionCookie(idToken, options); + } + }); + } + + /** + * Creates a new Firebase session cookie from the given ID token and options. The returned JWT + * can be set as a server-side session cookie with a custom cookie policy. + * + * @param idToken The Firebase ID token to exchange for a session cookie. + * @param options Additional options required to create the cookie. + * @return An {@code ApiFuture} which will complete successfully with a session cookie string. + * If an error occurs while generating the cookie or if the specified ID token is invalid, + * the future throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. + */ + public ApiFuture createSessionCookieAsync( + @NonNull String idToken, @NonNull SessionCookieOptions options) { + return new TaskToApiFuture<>(createSessionCookie(idToken, options)); + } + + /** + * Parses and verifies a Firebase session cookie. + * + *

If verified successfully, the returned {@code ApiFuture} completes with a parsed version of + * the cookie from which the UID and the other claims can be read. If the cookie is invalid, + * the future throws an exception indicating the failure. + * + *

This method does not check whether the cookie has been revoked. See + * {@link #verifySessionCookieAsync(String, boolean)}. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or + * unsuccessfully with the failure Exception. + */ + public ApiFuture verifySessionCookieAsync(String cookie) { + return new TaskToApiFuture<>(verifySessionCookie(cookie, false)); + } + + /** + * Parses and verifies a Firebase session cookie. + * + *

If {@code checkRevoked} is true, additionally verifies that the cookie has not been + * revoked. + * + *

If verified successfully, the returned {@code ApiFuture} completes with a parsed version of + * the cookie from which the UID and the other claims can be read. If the cookie is invalid or + * has been revoked while {@code checkRevoked} is true, the future throws an exception indicating + * the failure. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly + * revoked. + * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or + * unsuccessfully with the failure Exception. + */ + public ApiFuture verifySessionCookieAsync(String cookie, boolean checkRevoked) { + return new TaskToApiFuture<>(verifySessionCookie(cookie, checkRevoked)); + } + + private Task verifySessionCookie(final String cookie, final boolean checkRevoked) { + checkNotDestroyed(); + checkState(!Strings.isNullOrEmpty(projectId), + "Must initialize FirebaseApp with a project ID to call verifySessionCookie()"); + return call(new Callable() { + @Override + public FirebaseToken call() throws Exception { + FirebaseTokenVerifier firebaseTokenVerifier = + FirebaseTokenVerifier.createSessionCookieVerifier(projectId, keyManagers, clock); + FirebaseToken firebaseToken = FirebaseToken.parse(jsonFactory, cookie); + // This will throw a FirebaseAuthException with details on how the token is invalid. + firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken()); + + if (checkRevoked) { + checkRevoked(firebaseToken, "session cookie", + FirebaseUserManager.SESSION_COOKIE_REVOKED_ERROR); + } + return firebaseToken; + } + }); + } + + private void checkRevoked( + FirebaseToken firebaseToken, String label, String errorCode) throws FirebaseAuthException { + String uid = firebaseToken.getUid(); + UserRecord user = userManager.getUserById(uid); + long issuedAt = (long) firebaseToken.getClaims().get("iat"); + if (user.getTokensValidAfterTimestamp() > issuedAt * 1000) { + throw new FirebaseAuthException(errorCode, "Firebase " + label + " revoked"); + } + } + /** * Similar to {@link #createCustomTokenAsync(String)}, but returns a {@link Task}. * @@ -212,27 +309,14 @@ private Task verifyIdToken(final String token, final boolean chec return call(new Callable() { @Override public FirebaseToken call() throws Exception { - FirebaseTokenVerifier firebaseTokenVerifier = - new FirebaseTokenVerifier.Builder() - .setProjectId(projectId) - .setPublicKeysManager(googlePublicKeysManager) - .setClock(clock) - .build(); - + FirebaseTokenVerifier.createIdTokenVerifier(projectId, keyManagers, clock); FirebaseToken firebaseToken = FirebaseToken.parse(jsonFactory, token); - // This will throw a FirebaseAuthException with details on how the token is invalid. firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken()); - + if (checkRevoked) { - String uid = firebaseToken.getUid(); - UserRecord user = userManager.getUserById(uid); - long issuedAt = (long) firebaseToken.getClaims().get("iat"); - if (user.getTokensValidAfterTimestamp() > issuedAt * 1000) { - throw new FirebaseAuthException(FirebaseUserManager.ID_TOKEN_REVOKED_ERROR, - "Firebase auth token revoked"); - } + checkRevoked(firebaseToken, "auth token", FirebaseUserManager.ID_TOKEN_REVOKED_ERROR); } return firebaseToken; } @@ -241,7 +325,7 @@ public FirebaseToken call() throws Exception { private Task revokeRefreshTokens(String uid) { checkNotDestroyed(); - int currentTimeSeconds = (int) (System.currentTimeMillis() / 1000); + long currentTimeSeconds = System.currentTimeMillis() / 1000L; final UpdateRequest request = new UpdateRequest(uid).setValidSince(currentTimeSeconds); return call(new Callable() { @Override @@ -320,7 +404,7 @@ public ApiFuture verifyIdTokenAsync(final String token) { * failure. * * @param token A Firebase ID Token to verify and parse. - * @param checkRevoked A boolean denoting whether to check if the tokens were revoked. + * @param checkRevoked A boolean indicating whether to check if the tokens were revoked. * @return An {@code ApiFuture} which will complete successfully with the parsed token, or * unsuccessfully with the failure Exception. */ diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 79090ed2c..67720b6d2 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -60,6 +60,7 @@ class FirebaseUserManager { static final String USER_NOT_FOUND_ERROR = "user-not-found"; static final String INTERNAL_ERROR = "internal-error"; static final String ID_TOKEN_REVOKED_ERROR = "id-token-revoked"; + static final String SESSION_COOKIE_REVOKED_ERROR = "session-cookie-revoked"; // Map of server-side error codes to SDK error codes. // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors @@ -194,6 +195,20 @@ DownloadAccountResponse listUsers(int maxResults, String pageToken) throws Fireb return response; } + 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); + if (response != null) { + String cookie = (String) response.get("sessionCookie"); + if (!Strings.isNullOrEmpty(cookie)) { + return cookie; + } + } + throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create session cookie"); + } + 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"); diff --git a/src/main/java/com/google/firebase/auth/SessionCookieOptions.java b/src/main/java/com/google/firebase/auth/SessionCookieOptions.java new file mode 100644 index 000000000..d630780e8 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/SessionCookieOptions.java @@ -0,0 +1,76 @@ +/* + * 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.auth; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.concurrent.TimeUnit; + +/** + * A set of additional options that can be passed to + * {@link FirebaseAuth#createSessionCookieAsync(String, SessionCookieOptions)}. + */ +public class SessionCookieOptions { + + private final long expiresIn; + + private SessionCookieOptions(Builder builder) { + checkArgument(builder.expiresIn > TimeUnit.MINUTES.toMillis(5), + "expiresIn duration must be at least 5 minutes"); + checkArgument(builder.expiresIn < TimeUnit.DAYS.toMillis(14), + "expiresIn duration must be at most 14 days"); + this.expiresIn = builder.expiresIn; + } + + long getExpiresInSeconds() { + return TimeUnit.MILLISECONDS.toSeconds(expiresIn); + } + + /** + * Creates a new {@link Builder}. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private long expiresIn; + + private Builder() {} + + /** + * Sets the duration until the cookie is expired in milliseconds. Must be between 5 minutes + * and 14 days. + * + * @param expiresInMillis Time duration in milliseconds. + * @return This builder. + */ + public Builder setExpiresIn(long expiresInMillis) { + this.expiresIn = expiresInMillis; + return this; + } + + /** + * Creates a new {@link SessionCookieOptions} instance. + */ + public SessionCookieOptions build() { + return new SessionCookieOptions(this); + } + } + +} 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 1309318e0..f9d7233c2 100644 --- a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java +++ b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java @@ -83,7 +83,7 @@ public String createSignedCustomAuthTokenForUser( for (String key : developerClaims.keySet()) { if (reservedNames.contains(key)) { throw new IllegalArgumentException( - String.format("developer_claims can not contain a reserved key: %s", key)); + String.format("developerClaims must not contain a reserved key: %s", key)); } } GenericJson jsonObject = new GenericJson(); diff --git a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenVerifier.java b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenVerifier.java index db89a7ec5..98b269f0a 100644 --- a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenVerifier.java +++ b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenVerifier.java @@ -16,20 +16,20 @@ 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.auth.openidconnect.IdToken; import com.google.api.client.auth.openidconnect.IdToken.Payload; import com.google.api.client.auth.openidconnect.IdTokenVerifier; import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.json.webtoken.JsonWebSignature.Header; import com.google.api.client.util.ArrayMap; import com.google.api.client.util.Clock; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.internal.NonNull; import java.io.IOException; import java.math.BigDecimal; import java.security.GeneralSecurityException; @@ -45,45 +45,57 @@ */ public final class FirebaseTokenVerifier extends IdTokenVerifier { - @VisibleForTesting - static final String CLIENT_CERT_URL = + static final String ID_TOKEN_CERT_URL = "https://www.googleapis.com/robot/v1/metadata/x509/" + "securetoken@system.gserviceaccount.com"; - /** The default public keys manager for verifying projects use the correct public key. */ - public static final GooglePublicKeysManager DEFAULT_KEY_MANAGER = - new GooglePublicKeysManager.Builder(new NetHttpTransport.Builder().build(), new GsonFactory()) - .setClock(Clock.SYSTEM) - .setPublicCertsEncodedUrl(CLIENT_CERT_URL) - .build(); - - private static final String ISSUER_PREFIX = "https://securetoken.google.com/"; + static final String ID_TOKEN_ISSUER_PREFIX = "https://securetoken.google.com/"; + + static final String SESSION_COOKIE_CERT_URL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys"; + static final String SESSION_COOKIE_ISSUER_PREFIX = "https://session.firebase.google.com/"; + private static final String FIREBASE_AUDIENCE = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"; + private static final String ERROR_INVALID_CREDENTIAL = "ERROR_INVALID_CREDENTIAL"; private static final String ERROR_RUNTIME_EXCEPTION = "ERROR_RUNTIME_EXCEPTION"; private static final String PROJECT_ID_MATCH_MESSAGE = - " Make sure the ID token comes from the same Firebase project as the service account used to " + "Make sure the %s comes from the same Firebase project as the service account used to " + "authenticate this SDK."; - private static final String VERIFY_ID_TOKEN_DOCS_MESSAGE = - " See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to " - + "retrieve an ID token."; + private static final String VERIFY_TOKEN_DOCS_MESSAGE = + "See %s for details on how to retrieve %s."; private static final String ALGORITHM = "RS256"; - private String projectId; - private GooglePublicKeysManager publicKeysManager; - protected FirebaseTokenVerifier(Builder builder) { - super(builder); - Preconditions.checkArgument(builder.projectId != null, "projectId must be set"); + private final String projectId; + private final GooglePublicKeysManager publicKeysManager; + private final String method; + private final String shortName; + private final String articledShortName; + private final String projectIdMatchMessage; + private final String verifyTokenMessage; + + private FirebaseTokenVerifier(Builder builder) { + super(builder); + checkArgument(!Strings.isNullOrEmpty(builder.projectId), "projectId must be set"); + checkArgument(!Strings.isNullOrEmpty(builder.shortName), "shortName must be set"); + checkArgument(!Strings.isNullOrEmpty(builder.method), "method must be set"); this.projectId = builder.projectId; - this.publicKeysManager = builder.publicKeysManager; + this.shortName = builder.shortName; + this.articledShortName = prefixWithIndefiniteArticle(shortName); + this.method = builder.method; + this.publicKeysManager = checkNotNull(builder.publicKeysManager, + "publicKeysManager must be set"); + this.projectIdMatchMessage = String.format(PROJECT_ID_MATCH_MESSAGE, shortName); + this.verifyTokenMessage = String.format(VERIFY_TOKEN_DOCS_MESSAGE, builder.docUrl, + articledShortName); } /** * We are changing the semantics of the super-class method in order to provide more details on why * this is failing to the developer. */ - public boolean verifyTokenAndSignature(IdToken token) throws FirebaseAuthException { + public void verifyTokenAndSignature(IdToken token) throws FirebaseAuthException { Payload payload = token.getPayload(); Header header = token.getHeader(); String errorMessage = null; @@ -101,60 +113,68 @@ public boolean verifyTokenAndSignature(IdToken token) throws FirebaseAuthExcepti if (header.getKeyId() == null) { if (isCustomToken) { - errorMessage = "verifyIdToken() expects an ID token, but was given a custom token."; + errorMessage = String.format("%s expects %s, but was given a custom token.", + method, articledShortName); } else if (isLegacyCustomToken) { - errorMessage = "verifyIdToken() expects an ID token, but was given a legacy custom token."; + errorMessage = String.format("%s expects %s, but was given a legacy custom token.", + method, articledShortName); } else { - errorMessage = "Firebase ID token has no \"kid\" claim."; + errorMessage = String.format("Firebase %s has no \"kid\" claim.", shortName); } } else if (header.getAlgorithm() == null || !header.getAlgorithm().equals(ALGORITHM)) { errorMessage = String.format( - "Firebase ID token has incorrect algorithm. Expected \"%s\" but got \"%s\".", - ALGORITHM, header.getAlgorithm()); + "Firebase %s has incorrect algorithm. Expected \"%s\" but got \"%s\".", + shortName, ALGORITHM, header.getAlgorithm()); } else if (!token.verifyAudience(getAudience())) { errorMessage = String.format( - "Firebase ID token has incorrect \"aud\" (audience) claim. Expected \"%s\" but got " - + "\"%s\".", - concat(getAudience()), concat(token.getPayload().getAudienceAsList())); - errorMessage += PROJECT_ID_MATCH_MESSAGE; + "Firebase %s has incorrect \"aud\" (audience) claim. Expected \"%s\" but got " + + "\"%s\". %s", shortName, concat(getAudience()), + concat(token.getPayload().getAudienceAsList()), projectIdMatchMessage); } else if (!token.verifyIssuer(getIssuers())) { errorMessage = String.format( - "Firebase ID token has incorrect \"iss\" (issuer) claim. " - + "Expected \"%s\" but got \"%s\".", - concat(getIssuers()), token.getPayload().getIssuer()); - errorMessage += PROJECT_ID_MATCH_MESSAGE; + "Firebase %s has incorrect \"iss\" (issuer) claim. " + + "Expected \"%s\" but got \"%s\". %s", shortName, concat(getIssuers()), + token.getPayload().getIssuer(), projectIdMatchMessage); } else if (payload.getSubject() == null) { - errorMessage = "Firebase ID token has no \"sub\" (subject) claim."; + errorMessage = String.format("Firebase %s has no \"sub\" (subject) claim.", shortName); } else if (payload.getSubject().isEmpty()) { - errorMessage = "Firebase ID token has an empty string \"sub\" (subject) claim."; + errorMessage = String.format("Firebase %s has an empty string \"sub\" (subject) claim.", + shortName); } else if (payload.getSubject().length() > 128) { - errorMessage = "Firebase ID token has \"sub\" (subject) claim longer than 128 characters."; + errorMessage = String.format("Firebase %s has \"sub\" (subject) claim longer than " + + "128 characters.", shortName); } else if (!token.verifyTime(getClock().currentTimeMillis(), getAcceptableTimeSkewSeconds())) { errorMessage = - "Firebase ID token has expired or is not yet valid. Get a fresh token from your client " - + "app and try again."; + String.format("Firebase %s has expired or is not yet valid. Get a fresh %s and " + + "try again.", shortName, shortName); } if (errorMessage != null) { - errorMessage += VERIFY_ID_TOKEN_DOCS_MESSAGE; - throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, errorMessage); + throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, + String.format("%s %s", errorMessage, verifyTokenMessage)); } try { if (!verifySignature(token)) { - throw new FirebaseAuthException( - ERROR_INVALID_CREDENTIAL, - "Firebase ID token isn't signed by a valid public key." + VERIFY_ID_TOKEN_DOCS_MESSAGE); + throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, + String.format("Firebase %s isn't signed by a valid public key. %s", + shortName, verifyTokenMessage)); } } catch (IOException | GeneralSecurityException e) { throw new FirebaseAuthException( - ERROR_RUNTIME_EXCEPTION, "Error while verifying token signature.", e); + ERROR_RUNTIME_EXCEPTION, "Error while verifying signature.", e); } + } - return true; + private String prefixWithIndefiniteArticle(String word) { + if ("aeiouAEIOU".indexOf(word.charAt(0)) < 0) { + return "a " + word; + } else { + return "an " + word; + } } private String concat(Collection collection) { @@ -168,8 +188,6 @@ private String concat(Collection collection) { /** * Verifies the cryptographic signature on the FirebaseToken. Can block on a web request to fetch * the keys if they have expired. - * - *

TODO: Wrap these blocking steps in a Task. */ private boolean verifySignature(IdToken token) throws GeneralSecurityException, IOException { for (PublicKey key : publicKeysManager.getPublicKeys()) { @@ -184,45 +202,86 @@ public String getProjectId() { return projectId; } - public static GooglePublicKeysManager buildGooglePublicKeysManager(HttpTransport transport) { - return new GooglePublicKeysManager.Builder(transport, new GsonFactory()) - .setClock(Clock.SYSTEM) - .setPublicCertsEncodedUrl(FirebaseTokenVerifier.CLIENT_CERT_URL) - .build(); - } - /** * Builder for {@link FirebaseTokenVerifier}. */ public static class Builder extends IdTokenVerifier.Builder { - String projectId; - - GooglePublicKeysManager publicKeysManager = DEFAULT_KEY_MANAGER; + private String projectId; + private String shortName; + private String method; + private String docUrl; + private GooglePublicKeysManager publicKeysManager; public String getProjectId() { return projectId; } - public Builder setProjectId(String projectId) { + /** + * Sets the project ID and a URL prefix for the issuer (iss) claim. The full issuer claim + * is the concatenation of the prefix and the project ID. + * + * @param issuerPrefix A URL prefix. + * @param projectId A Firebase project ID. + * @return This builder. + */ + public Builder setProjectId(String issuerPrefix, String projectId) { this.projectId = projectId; - - this.setIssuer(ISSUER_PREFIX + projectId); this.setAudience(Collections.singleton(projectId)); + this.setIssuer(issuerPrefix + projectId); + return this; + } + + /** + * Sets the short name of the type of tokens being validated (e.g. ID token, session cookie). + * + * @param shortName A short string identifier. + * @return This builder. + */ + public Builder setShortName(String shortName) { + this.shortName = shortName; + return this; + } + + /** + * Sets the name of the operation that triggers token verification (e.g. verifyIdToken()) + * + * @param method A method name. + * @return This builder. + */ + public Builder setMethod(String method) { + this.method = method; + return this; + } + /** + * A URL to public documentation where more information about token verification can be found. + * + * @param docUrl A documentation URL. + * @return This builder. + */ + public Builder setDocUrl(String docUrl) { + this.docUrl = docUrl; return this; } + /** + * Sets the {@code Clock} instance to be used to compare token issue and expiry times. + * + * @param clock A {@code Clock} instance. + * @return This builder. + */ @Override public Builder setClock(Clock clock) { return (Builder) super.setClock(clock); } - public GooglePublicKeysManager getPublicKeyManager() { - return publicKeysManager; - } - - /** Override the GooglePublicKeysManager from the default. */ + /** + * Overrides the GooglePublicKeysManager from the default. + * + * @param publicKeysManager A public keys manager. + * @return This builder. + */ public Builder setPublicKeysManager(GooglePublicKeysManager publicKeysManager) { this.publicKeysManager = publicKeysManager; return this; @@ -233,4 +292,44 @@ public FirebaseTokenVerifier build() { return new FirebaseTokenVerifier(this); } } + + /** + * Creates a new {@link FirebaseTokenVerifier} for verifying Firebase ID tokens. + * + * @param projectId Project ID string + * @param keyManagers {@link KeyManagers} instance with public key managers to use + * @param clock {@code Clock} instance for Google API client + * @return A new {@link FirebaseTokenVerifier} instance + */ + @NonNull public static FirebaseTokenVerifier createIdTokenVerifier( + @NonNull String projectId, @NonNull KeyManagers keyManagers, @NonNull Clock clock) { + return new FirebaseTokenVerifier.Builder() + .setProjectId(ID_TOKEN_ISSUER_PREFIX, projectId) + .setPublicKeysManager(keyManagers.getIdTokenKeysManager()) + .setShortName("ID token") + .setMethod("verifyIdToken()") + .setDocUrl("https://firebase.google.com/docs/auth/admin/verify-id-tokens") + .setClock(clock) + .build(); + } + + /** + * Creates a new {@link FirebaseTokenVerifier} for verifying Firebase ID tokens. + * + * @param projectId Project ID string + * @param keyManagers {@link KeyManagers} instance with public key managers to use + * @param clock {@code Clock} instance for Google API client + * @return A new {@link FirebaseTokenVerifier} instance + */ + @NonNull public static FirebaseTokenVerifier createSessionCookieVerifier( + @NonNull String projectId, @NonNull KeyManagers keyManagers, @NonNull Clock clock) { + return new FirebaseTokenVerifier.Builder() + .setProjectId(SESSION_COOKIE_ISSUER_PREFIX, projectId) + .setPublicKeysManager(keyManagers.getSessionCookieKeysManager()) + .setShortName("session cookie") + .setMethod("verifySessionCookie()") + .setDocUrl("https://firebase.google.com/docs/auth/admin/manage-cookies") + .setClock(clock) + .build(); + } } diff --git a/src/main/java/com/google/firebase/auth/internal/KeyManagers.java b/src/main/java/com/google/firebase/auth/internal/KeyManagers.java new file mode 100644 index 000000000..b22837a1a --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/KeyManagers.java @@ -0,0 +1,85 @@ +/* + * 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.auth.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.util.Clock; +import com.google.common.annotations.VisibleForTesting; +import com.google.firebase.FirebaseApp; +import com.google.firebase.internal.NonNull; + +/** + * A utility for initializing and keeping tack of the various public key manager instances + * used by {@link com.google.firebase.auth.FirebaseAuth}. + */ +public class KeyManagers { + + private final GooglePublicKeysManager idTokenKeysManager; + private final GooglePublicKeysManager sessionCookieKeysManager; + + private KeyManagers( + GooglePublicKeysManager idTokenKeysManager, + GooglePublicKeysManager sessionCookieKeysManager) { + this.idTokenKeysManager = checkNotNull(idTokenKeysManager); + this.sessionCookieKeysManager = checkNotNull(sessionCookieKeysManager); + } + + /** + * Returns the key manager that should be used for ID token verification. + */ + GooglePublicKeysManager getIdTokenKeysManager() { + return idTokenKeysManager; + } + + /** + * Returns the key manager that should be used for session cookie verification. + */ + GooglePublicKeysManager getSessionCookieKeysManager() { + return sessionCookieKeysManager; + } + + /** + * Initialize a new set of key managers for the specified app using the given clock. + * + * @param app A {@link FirebaseApp} instance. + * @param clock A {@code Clock} to be used with Google API client. + * @return A new {@link KeyManagers} instance. + */ + public static KeyManagers getDefault(@NonNull FirebaseApp app, @NonNull Clock clock) { + HttpTransport transport = app.getOptions().getHttpTransport(); + return getDefault(transport, clock); + } + + @VisibleForTesting + static KeyManagers getDefault(HttpTransport transport, Clock clock) { + return new KeyManagers( + createPublicKeysManager(transport, clock, FirebaseTokenVerifier.ID_TOKEN_CERT_URL), + createPublicKeysManager(transport, clock, FirebaseTokenVerifier.SESSION_COOKIE_CERT_URL)); + } + + private static GooglePublicKeysManager createPublicKeysManager( + HttpTransport transport, Clock clock, String certUrl) { + return new GooglePublicKeysManager.Builder(transport, new GsonFactory()) + .setClock(clock) + .setPublicCertsEncodedUrl(certUrl) + .build(); + } +} diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 2995fe35a..4e71d7f4b 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -35,6 +35,7 @@ import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutureCallback; import com.google.api.core.ApiFutures; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.auth.UserRecord.CreateRequest; @@ -48,6 +49,7 @@ import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -64,7 +66,7 @@ public class FirebaseAuthIT { private static FirebaseAuth auth; @BeforeClass - public static void setUpClass() throws Exception { + public static void setUpClass() { FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp(); auth = FirebaseAuth.getInstance(masterApp); } @@ -370,7 +372,7 @@ public void testVerifyIdToken() throws Exception { decoded = auth.verifyIdTokenAsync(idToken, false).get(); assertEquals("user2", decoded.getUid()); try { - decoded = auth.verifyIdTokenAsync(idToken, true).get(); + auth.verifyIdTokenAsync(idToken, true).get(); fail("expecting exception"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); @@ -383,6 +385,42 @@ public void testVerifyIdToken() throws Exception { auth.deleteUserAsync("user2"); } + @Test + public void testVerifySessionCookie() throws Exception { + String customToken = auth.createCustomTokenAsync("user3").get(); + String idToken = signInWithCustomToken(customToken); + + SessionCookieOptions options = SessionCookieOptions.builder() + .setExpiresIn(TimeUnit.HOURS.toMillis(1)) + .build(); + String sessionCookie = auth.createSessionCookieAsync(idToken, options).get(); + assertFalse(Strings.isNullOrEmpty(sessionCookie)); + + FirebaseToken decoded = auth.verifySessionCookieAsync(sessionCookie).get(); + assertEquals("user3", decoded.getUid()); + decoded = auth.verifySessionCookieAsync(sessionCookie, true).get(); + assertEquals("user3", decoded.getUid()); + Thread.sleep(1000); + + auth.revokeRefreshTokensAsync("user3").get(); + decoded = auth.verifySessionCookieAsync(sessionCookie, false).get(); + assertEquals("user3", decoded.getUid()); + try { + auth.verifySessionCookieAsync(sessionCookie, true).get(); + fail("expecting exception"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(FirebaseUserManager.SESSION_COOKIE_REVOKED_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + + idToken = signInWithCustomToken(customToken); + sessionCookie = auth.createSessionCookieAsync(idToken, options).get(); + decoded = auth.verifySessionCookieAsync(sessionCookie, true).get(); + assertEquals("user3", decoded.getUid()); + auth.deleteUserAsync("user3"); + } + @Test public void testCustomTokenWithClaims() throws Exception { Map devClaims = ImmutableMap.of( diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 3f29b06a3..25f5cde6c 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -53,6 +53,7 @@ import java.util.Map; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Test; @@ -251,6 +252,79 @@ public void testDeleteUser() throws Exception { checkRequestHeaders(interceptor); } + @Test + public void testCreateSessionCookie() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("createSessionCookie.json")); + SessionCookieOptions options = SessionCookieOptions.builder() + .setExpiresIn(TimeUnit.HOURS.toMillis(1)) + .build(); + String cookie = FirebaseAuth.getInstance().createSessionCookieAsync("testToken", options).get(); + assertEquals("MockCookieString", cookie); + 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("testToken", parsed.get("idToken")); + assertEquals(new BigDecimal(3600), parsed.get("validDuration")); + } + + @Test + public void testCreateSessionInvalidArguments() { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + SessionCookieOptions options = SessionCookieOptions.builder() + .setExpiresIn(TimeUnit.HOURS.toMillis(1)) + .build(); + try { + FirebaseAuth.getInstance().createSessionCookieAsync(null, options); + fail("No error thrown for null id token"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + FirebaseAuth.getInstance().createSessionCookieAsync("", options); + fail("No error thrown for empty id token"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + FirebaseAuth.getInstance().createSessionCookieAsync("idToken", null); + fail("No error thrown for null options"); + } catch (NullPointerException expected) { + // expected + } + } + + @Test + public void testInvalidSessionCookieOptions() { + try { + SessionCookieOptions.builder().build(); + fail("No error thrown for unspecified expiresIn"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + SessionCookieOptions.builder().setExpiresIn(TimeUnit.SECONDS.toMillis(299)).build(); + fail("No error thrown for low expiresIn"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + SessionCookieOptions.builder().setExpiresIn(TimeUnit.DAYS.toMillis(14) + 1).build(); + fail("No error thrown for high expiresIn"); + } catch (IllegalArgumentException expected) { + // expected + } + } + @Test public void testGetUserHttpError() throws Exception { List operations = ImmutableList.builder() diff --git a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java index e0df09c73..da92a75b8 100644 --- a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java +++ b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java @@ -20,7 +20,6 @@ import static org.junit.Assert.assertTrue; import com.google.api.client.auth.openidconnect.IdToken; -import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; import com.google.api.client.http.LowLevelHttpRequest; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.gson.GsonFactory; @@ -58,7 +57,10 @@ public class FirebaseTokenVerifierTest { private static final JsonFactory FACTORY = new GsonFactory(); private static final FixedClock CLOCK = new FixedClock(2002000L * 1000); private static final String PROJECT_ID = "proj-test-101"; - private static final String ISSUER = "https://securetoken.google.com/" + PROJECT_ID; + private static final String TOKEN_ISSUER = + FirebaseTokenVerifier.ID_TOKEN_ISSUER_PREFIX + PROJECT_ID; + private static final String COOKIE_ISSUER = + FirebaseTokenVerifier.SESSION_COOKIE_ISSUER_PREFIX + PROJECT_ID; private static final String PRIVATE_KEY_ID = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd"; private static final String UID = "someUid"; private static final String ALGORITHM = "RS256"; @@ -68,9 +70,12 @@ public class FirebaseTokenVerifierTest { + "yc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWiwuLzsnW11cXDw" + "-P1wie318In0sInYiOjAsImlhdCI6MTQ4MDk4Mj" + "U2NH0.ZWEpoHgIPCAz8Q-cNFBS8jiqClTJ3j27yuRkQo-QxyI"; + @Rule public ExpectedException thrown = ExpectedException.none(); + private PrivateKey privateKey; - private FirebaseTokenVerifier verifier; + private FirebaseTokenVerifier idTokenVerifier; + private FirebaseTokenVerifier cookieVerifier; private void initCrypto(String privateKey, String certificate) throws NoSuchAlgorithmException, InvalidKeySpecException { @@ -84,17 +89,12 @@ private void initCrypto(String privateKey, String certificate) .setLowLevelHttpResponse( new MockLowLevelHttpResponse().setContent(serviceAccountCertificates)) .build(); + KeyManagers keyManagers = KeyManagers.getDefault(mockTransport, CLOCK); this.privateKey = KeyFactory.getInstance("RSA").generatePrivate(spec); - this.verifier = - new FirebaseTokenVerifier.Builder() - .setClock(CLOCK) - .setPublicKeysManager( - new GooglePublicKeysManager.Builder(mockTransport, FACTORY) - .setClock(CLOCK) - .setPublicCertsEncodedUrl(FirebaseTokenVerifier.CLIENT_CERT_URL) - .build()) - .setProjectId(PROJECT_ID) - .build(); + this.idTokenVerifier = FirebaseTokenVerifier.createIdTokenVerifier( + PROJECT_ID, keyManagers, CLOCK); + this.cookieVerifier = FirebaseTokenVerifier.createSessionCookieVerifier( + PROJECT_ID, keyManagers, CLOCK); } @Before @@ -102,7 +102,7 @@ public void setUp() throws Exception { initCrypto(ServiceAccount.EDITOR.getPrivateKey(), ServiceAccount.EDITOR.getCert()); } - private JsonWebSignature.Header createHeader() throws Exception { + private JsonWebSignature.Header createHeader() { JsonWebSignature.Header header = new JsonWebSignature.Header(); header.setAlgorithm(ALGORITHM); header.setType("JWT"); @@ -110,9 +110,19 @@ private JsonWebSignature.Header createHeader() throws Exception { return header; } - private JsonWebToken.Payload createPayload() { + private JsonWebToken.Payload createTokenPayload() { JsonWebToken.Payload payload = new JsonWebToken.Payload(); - payload.setIssuer(ISSUER); + payload.setIssuer(TOKEN_ISSUER); + payload.setAudience(PROJECT_ID); + payload.setIssuedAtTimeSeconds(CLOCK.currentTimeMillis() / 1000); + payload.setExpirationTimeSeconds(CLOCK.currentTimeMillis() / 1000 + 3600); + payload.setSubject(UID); + return payload; + } + + private JsonWebToken.Payload createCookiePayload() { + JsonWebToken.Payload payload = new JsonWebToken.Payload(); + payload.setIssuer(COOKIE_ISSUER); payload.setAudience(PROJECT_ID); payload.setIssuedAtTimeSeconds(CLOCK.currentTimeMillis() / 1000); payload.setExpirationTimeSeconds(CLOCK.currentTimeMillis() / 1000 + 3600); @@ -129,13 +139,36 @@ private String createToken(JsonWebSignature.Header header, JsonWebToken.Payload public void verifyToken() throws Exception { FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), createPayload())); + FACTORY, createToken(createHeader(), createTokenPayload())); IdToken.Payload payload = (IdToken.Payload) token.getClaims(); assertTrue(payload.getAudienceAsList().contains(PROJECT_ID)); - assertEquals(ISSUER, payload.getIssuer()); + assertEquals(TOKEN_ISSUER, payload.getIssuer()); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(createHeader(), createCookiePayload())); + thrown.expectMessage("Firebase ID token has incorrect \"iss\" (issuer) claim."); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + } + + @Test + public void verifyCookie() throws Exception { + FirebaseToken token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(createHeader(), createCookiePayload())); + + IdToken.Payload payload = (IdToken.Payload) token.getClaims(); + assertTrue(payload.getAudienceAsList().contains(PROJECT_ID)); + assertEquals(COOKIE_ISSUER, payload.getIssuer()); + cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + + token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(createHeader(), createTokenPayload())); + thrown.expectMessage("Firebase session cookie has incorrect \"iss\" (issuer) claim."); + cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test @@ -144,23 +177,49 @@ public void verifyTokenFailure_MissingKeyId() throws Exception { header.setKeyId(null); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(header, createPayload())); + FACTORY, createToken(header, createTokenPayload())); thrown.expectMessage("Firebase ID token has no \"kid\" claim."); - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + } + + @Test + public void verifyCookieFailure_MissingKeyId() throws Exception { + Header header = createHeader(); + header.setKeyId(null); + FirebaseToken token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(header, createCookiePayload())); + thrown.expectMessage("Firebase session cookie has no \"kid\" claim."); + cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_MissingKeyId_CustomToken() throws Exception { Header header = createHeader(); header.setKeyId(null); - Payload payload = createPayload(); + Payload payload = createTokenPayload(); payload.setAudience( "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit" + ".v1.IdentityToolkit"); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken(FACTORY, createToken(header, payload)); thrown.expectMessage("verifyIdToken() expects an ID token, but was given a custom token."); - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + } + + @Test + public void verifyCookieFailure_MissingKeyId_CustomToken() throws Exception { + Header header = createHeader(); + header.setKeyId(null); + Payload payload = createCookiePayload(); + payload.setAudience( + "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit" + + ".v1.IdentityToolkit"); + FirebaseToken token = + TestOnlyImplFirebaseAuthTrampolines.parseToken(FACTORY, createToken(header, payload)); + thrown.expectMessage( + "verifySessionCookie() expects a session cookie, but was given a custom token."); + cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test @@ -169,14 +228,25 @@ public void verifyTokenFailure_IncorrectAlgorithm() throws Exception { header.setAlgorithm("HS256"); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(header, createPayload())); + FACTORY, createToken(header, createTokenPayload())); thrown.expectMessage("Firebase ID token has incorrect algorithm."); - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + } + + @Test + public void verifyCookieFailure_IncorrectAlgorithm() throws Exception { + Header header = createHeader(); + header.setAlgorithm("HS256"); + FirebaseToken token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(header, createCookiePayload())); + thrown.expectMessage("Firebase session cookie has incorrect algorithm."); + cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_IncorrectAudience() throws Exception { - Payload payload = createPayload(); + Payload payload = createTokenPayload(); payload.setAudience( "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1." + "IdentityToolkit"); @@ -184,45 +254,91 @@ public void verifyTokenFailure_IncorrectAudience() throws Exception { TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), payload)); thrown.expectMessage("Firebase ID token has incorrect \"aud\" (audience) claim."); - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + } + + @Test + public void verifyCookieFailure_IncorrectAudience() throws Exception { + Payload payload = createCookiePayload(); + payload.setAudience( + "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1." + + "IdentityToolkit"); + FirebaseToken token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(createHeader(), payload)); + thrown.expectMessage("Firebase session cookie has incorrect \"aud\" (audience) claim."); + cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_IncorrectIssuer() throws Exception { - Payload payload = createPayload(); + Payload payload = createTokenPayload(); payload.setIssuer("https://foobar.google.com/" + PROJECT_ID); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), payload)); thrown.expectMessage("Firebase ID token has incorrect \"iss\" (issuer) claim."); - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + } + + @Test + public void verifyCookieFailure_IncorrectIssuer() throws Exception { + Payload payload = createCookiePayload(); + payload.setIssuer("https://foobar.google.com/" + PROJECT_ID); + FirebaseToken token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(createHeader(), payload)); + thrown.expectMessage("Firebase session cookie has incorrect \"iss\" (issuer) claim."); + cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_MissingSubject() throws Exception { - Payload payload = createPayload(); + Payload payload = createTokenPayload(); payload.setSubject(null); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), payload)); thrown.expectMessage("Firebase ID token has no \"sub\" (subject) claim."); - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + } + + @Test + public void verifyCookieFailure_MissingSubject() throws Exception { + Payload payload = createCookiePayload(); + payload.setSubject(null); + FirebaseToken token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(createHeader(), payload)); + thrown.expectMessage("Firebase session cookie has no \"sub\" (subject) claim."); + cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_EmptySubject() throws Exception { - Payload payload = createPayload(); + Payload payload = createTokenPayload(); payload.setSubject(""); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), payload)); thrown.expectMessage("Firebase ID token has an empty string \"sub\" (subject) claim."); - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + } + + @Test + public void verifyCookieFailure_EmptySubject() throws Exception { + Payload payload = createCookiePayload(); + payload.setSubject(""); + FirebaseToken token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(createHeader(), payload)); + thrown.expectMessage("Firebase session cookie has an empty string \"sub\" (subject) claim."); + cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_LongSubject() throws Exception { - Payload payload = createPayload(); + Payload payload = createTokenPayload(); payload.setSubject( "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv" + "wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"); @@ -231,31 +347,69 @@ public void verifyTokenFailure_LongSubject() throws Exception { FACTORY, createToken(createHeader(), payload)); thrown.expectMessage( "Firebase ID token has \"sub\" (subject) claim longer than 128 characters."); - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + } + + @Test + public void verifyCookieFailure_LongSubject() throws Exception { + Payload payload = createCookiePayload(); + payload.setSubject( + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv" + + "wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"); + FirebaseToken token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(createHeader(), payload)); + thrown.expectMessage( + "Firebase session cookie has \"sub\" (subject) claim longer than 128 characters."); + cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_NotYetIssued() throws Exception { - Payload payload = createPayload(); + Payload payload = createTokenPayload(); payload.setIssuedAtTimeSeconds(System.currentTimeMillis() / 1000); payload.setExpirationTimeSeconds(System.currentTimeMillis() / 1000 + 3600); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), payload)); thrown.expectMessage("Firebase ID token has expired or is not yet valid."); - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + } + + @Test + public void verifyCookieFailure_NotYetIssued() throws Exception { + Payload payload = createCookiePayload(); + payload.setIssuedAtTimeSeconds(System.currentTimeMillis() / 1000); + payload.setExpirationTimeSeconds(System.currentTimeMillis() / 1000 + 3600); + FirebaseToken token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(createHeader(), payload)); + thrown.expectMessage("Firebase session cookie has expired or is not yet valid."); + cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenFailure_Expired() throws Exception { - Payload payload = createPayload(); + Payload payload = createTokenPayload(); payload.setIssuedAtTimeSeconds(0L); payload.setExpirationTimeSeconds(3600L); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( FACTORY, createToken(createHeader(), payload)); thrown.expectMessage("Firebase ID token has expired or is not yet valid."); - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + } + + @Test + public void verifyCookieFailure_Expired() throws Exception { + Payload payload = createCookiePayload(); + payload.setIssuedAtTimeSeconds(0L); + payload.setExpirationTimeSeconds(3600L); + FirebaseToken token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(createHeader(), payload)); + thrown.expectMessage("Firebase session cookie has expired or is not yet valid."); + cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test @@ -263,16 +417,22 @@ public void verifyTokenFailure_WrongCert() throws Exception { initCrypto(ServiceAccount.OWNER.getPrivateKey(), ServiceAccount.NONE.getCert()); FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), createPayload())); + FACTORY, createToken(createHeader(), createTokenPayload())); thrown.expectMessage("Firebase ID token isn't signed by a valid public key."); - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + + token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(createHeader(), createCookiePayload())); + thrown.expectMessage("Firebase session cookie isn't signed by a valid public key."); + cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } @Test public void verifyTokenCertificateError() throws Exception { FirebaseToken token = TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), createPayload())); + FACTORY, createToken(createHeader(), createTokenPayload())); MockHttpTransport mockTransport = new MockHttpTransport() { @Override @@ -280,15 +440,21 @@ public LowLevelHttpRequest buildRequest(String method, String url) throws IOExce throw new IOException("Expected error"); } }; - FirebaseTokenVerifier verifier = new FirebaseTokenVerifier.Builder() - .setClock(CLOCK) - .setPublicKeysManager( - new GooglePublicKeysManager.Builder(mockTransport, FACTORY) - .setClock(CLOCK) - .setPublicCertsEncodedUrl(FirebaseTokenVerifier.CLIENT_CERT_URL) - .build()) - .setProjectId(PROJECT_ID) - .build(); + KeyManagers keyManagers = KeyManagers.getDefault(mockTransport, CLOCK); + FirebaseTokenVerifier verifier = FirebaseTokenVerifier.createIdTokenVerifier( + PROJECT_ID, keyManagers, CLOCK); + try { + verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + Assert.fail("No exception thrown"); + } catch (FirebaseAuthException expected) { + assertTrue(expected.getCause() instanceof IOException); + assertEquals("Expected error", expected.getCause().getMessage()); + } + + token = + TestOnlyImplFirebaseAuthTrampolines.parseToken( + FACTORY, createToken(createHeader(), createCookiePayload())); + verifier = FirebaseTokenVerifier.createSessionCookieVerifier(PROJECT_ID, keyManagers, CLOCK); try { verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); Assert.fail("No exception thrown"); @@ -305,6 +471,6 @@ public void legacyCustomToken() throws Exception { TestOnlyImplFirebaseAuthTrampolines.parseToken(FACTORY, LEGACY_CUSTOM_TOKEN); thrown.expectMessage( "verifyIdToken() expects an ID token, but was given a legacy custom token."); - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); + idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); } } diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java index dca85e039..ed219bd86 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java @@ -21,14 +21,25 @@ import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.FirebaseToken; import com.google.firebase.auth.ListUsersPage; +import com.google.firebase.auth.SessionCookieOptions; import com.google.firebase.auth.UserRecord; import com.google.firebase.auth.UserRecord.CreateRequest; import com.google.firebase.auth.UserRecord.UpdateRequest; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.FirebaseDatabase; +import java.net.URI; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import javax.ws.rs.Consumes; +import javax.ws.rs.CookieParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; /** * Auth snippets for documentation. @@ -259,6 +270,138 @@ public static void revokeIdTokens( userData.put("revokeTime", revocationSecond); ref.setValueAsync(userData).get(); // [END save_revocation_in_db] + } + + static class LoginRequest { + String idToken; + + String getIdToken() { + return idToken; + } + } + + // [START session_login] + @POST + @Path("/sessionLogin") + @Consumes("application/json") + public Response createSessionCookie(LoginRequest request) { + // Get the ID token sent by the client + String idToken = request.getIdToken(); + // Set session expiration to 5 days. + long expiresIn = TimeUnit.DAYS.toMillis(5); + SessionCookieOptions options = SessionCookieOptions.builder() + .setExpiresIn(expiresIn) + .build(); + try { + // Create the session cookie. This will also verify the ID token in the process. + // The session cookie will have the same claims as the ID token. + String sessionCookie = FirebaseAuth.getInstance().createSessionCookieAsync( + idToken, options).get(); + // Set cookie policy parameters as required. + NewCookie cookie = new NewCookie("session", sessionCookie /* ... other parameters */); + return Response.ok().cookie(cookie).build(); + } catch (Exception e) { + return Response.status(Status.UNAUTHORIZED).entity("Failed to create a session cookie") + .build(); + } + } + // [END session_login] + + public Response checkAuthTime(String idToken) throws Exception { + // [START check_auth_time] + // To ensure that cookies are set only on recently signed in users, check auth_time in + // ID token before creating a cookie. + FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdTokenAsync(idToken).get(); + long authTimeMillis = TimeUnit.SECONDS.toMillis( + (long) decodedToken.getClaims().get("auth_time")); + + // Only process if the user signed in within the last 5 minutes. + if (System.currentTimeMillis() - authTimeMillis < TimeUnit.MINUTES.toMillis(5)) { + long expiresIn = TimeUnit.DAYS.toMillis(5); + SessionCookieOptions options = SessionCookieOptions.builder() + .setExpiresIn(expiresIn) + .build(); + String sessionCookie = FirebaseAuth.getInstance().createSessionCookieAsync( + idToken, options).get(); + // Set cookie policy parameters as required. + NewCookie cookie = new NewCookie("session", sessionCookie); + return Response.ok().cookie(cookie).build(); + } + // User did not sign in recently. To guard against ID token theft, require + // re-authentication. + return Response.status(Status.UNAUTHORIZED).entity("Recent sign in required").build(); + // [END check_auth_time] + } + + private Response serveContentForUser(FirebaseToken decodedToken) { + return null; + } + private Response serveContentForAdmin(FirebaseToken decodedToken) { + return null; + } + + // [START session_verify] + @POST + @Path("/profile") + public Response verifySessionCookie(@CookieParam("session") Cookie cookie) { + String sessionCookie = cookie.getValue(); + try { + // Verify the session cookie. In this case an additional check is added to detect + // if the user's Firebase session was revoked, user deleted/disabled, etc. + final boolean checkRevoked = true; + FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookieAsync( + sessionCookie, checkRevoked).get(); + return serveContentForUser(decodedToken); + } catch (Exception e) { + // Session cookie is unavailable, invalid or revoked. Force user to login. + return Response.temporaryRedirect(URI.create("/login")).build(); + } + } + // [END session_verify] + + public Response checkPermissions(String sessionCookie) { + // [START session_verify_with_permission_check] + try { + final boolean checkRevoked = true; + FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookieAsync( + sessionCookie, checkRevoked).get(); + if (Boolean.TRUE.equals(decodedToken.getClaims().get("admin"))) { + return serveContentForAdmin(decodedToken); + } + return Response.status(Status.UNAUTHORIZED).entity("Insufficient permissions").build(); + } catch (Exception e) { + // Session cookie is unavailable, invalid or revoked. Force user to login. + return Response.temporaryRedirect(URI.create("/login")).build(); + } + // [END session_verify_with_permission_check] + } + + // [START session_clear] + @POST + @Path("/sessionLogout") + public Response clearSessionCookie(@CookieParam("session") Cookie cookie) { + final int maxAge = 0; + NewCookie newCookie = new NewCookie(cookie, null, maxAge, true); + return Response.temporaryRedirect(URI.create("/login")).cookie(newCookie).build(); + } + // [END session_clear] + + // [START session_clear_and_revoke] + @POST + @Path("/sessionLogout") + public Response clearSessionCookieAndRevoke(@CookieParam("session") Cookie cookie) { + String sessionCookie = cookie.getValue(); + try { + FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookieAsync( + sessionCookie).get(); + FirebaseAuth.getInstance().revokeRefreshTokensAsync(decodedToken.getUid()).get(); + final int maxAge = 0; + NewCookie newCookie = new NewCookie(cookie, null, maxAge, true); + return Response.temporaryRedirect(URI.create("/login")).cookie(newCookie).build(); + } catch (Exception e) { + return Response.temporaryRedirect(URI.create("/login")).build(); + } } -} \ No newline at end of file + // [END session_clear_and_revoke] +} diff --git a/src/test/resources/createSessionCookie.json b/src/test/resources/createSessionCookie.json new file mode 100644 index 000000000..885fd9a48 --- /dev/null +++ b/src/test/resources/createSessionCookie.json @@ -0,0 +1,3 @@ +{ + "sessionCookie": "MockCookieString" +} From 8e3ee433b180ec3e776334b8e1e6f8aacd0bceb1 Mon Sep 17 00:00:00 2001 From: Caspar Nonclercq Date: Mon, 23 Apr 2018 18:06:19 +0100 Subject: [PATCH 007/456] Add handling of null response content (#167) * Add handling of null response content If an error response comes back with no content, the json factory throws a NullPointerException when it attempts to read the response, which hides the response code from the client. * Change catch NPE for null check in response content. --- .../firebase/messaging/FirebaseMessaging.java | 13 ++++++----- .../messaging/FirebaseMessagingTest.java | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 2daec8490..e303ac863 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -253,13 +253,14 @@ private String makeSendRequest(Message message, private void handleSendHttpError(HttpResponseException e) throws FirebaseMessagingException { MessagingServiceErrorResponse response = new MessagingServiceErrorResponse(); - try { - JsonParser parser = jsonFactory.createJsonParser(e.getContent()); - parser.parseAndClose(response); - } catch (IOException ignored) { - // ignored + if (e.getContent() != null) { + try { + JsonParser parser = jsonFactory.createJsonParser(e.getContent()); + parser.parseAndClose(response); + } catch (IOException ignored) { + // ignored + } } - String code = FCM_ERROR_CODES.get(response.getErrorCode()); if (code == null) { code = 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 2872d8a19..d31668093 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -172,6 +172,29 @@ public void testSendError() throws Exception { } } + @Test + public void testSendErrorWithZeroContentResponse() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + FirebaseMessaging messaging = initMessaging(response); + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setZeroContent(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + try { + messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("unknown-error", error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: " + code + "; body: null", + error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkRequestHeader(interceptor); + } + } + @Test public void testSendErrorWithDetails() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); From 15b79290747e1ee4594ee883899706c708e1e5e0 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 24 Apr 2018 13:26:33 -0700 Subject: [PATCH 008/456] Upgraded GCS and Firestore to latest (#168) --- CHANGELOG.md | 4 ++-- pom.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 352f2c06b..e4365deb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ creating a long-lived session cookie given a valid ID token. - [added] A new `FirebaseAuth.verifySessionCookieAsync()` method for verifying a given cookie string is valid. -- [fixed] Upgraded Cloud Firestore dependency version to 0.44.0-beta. -- [fixed] Upgraded Cloud Storage dependency version to 1.26.0. +- [fixed] Upgraded Cloud Firestore dependency version to 0.45.0-beta. +- [fixed] Upgraded Cloud Storage dependency version to 1.27.0. - [fixed] Upgraded Netty dependency version to 4.1.22. # v5.10.0 diff --git a/pom.xml b/pom.xml index 395c62137..d750657ff 100644 --- a/pom.xml +++ b/pom.xml @@ -406,12 +406,12 @@ com.google.cloud google-cloud-storage - 1.26.0 + 1.27.0 com.google.cloud google-cloud-firestore - 0.44.0-beta + 0.45.0-beta From 031965fb160600df6c9586a6344675c96395901f Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 25 Apr 2018 11:27:13 -0700 Subject: [PATCH 009/456] Staging 5.11.0 Release (#169) * Updating CHANGELOG for 5.11.0 release. * [maven-release-plugin] prepare release v5.11.0 * [maven-release-plugin] prepare for next development iteration * Updated changelog --- CHANGELOG.md | 3 +++ pom.xml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4365deb1..9040ff20a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased + +# v5.11.0 + - [added] A new `FirebaseAuth.createSessionCookieAsync()` method for creating a long-lived session cookie given a valid ID token. - [added] A new `FirebaseAuth.verifySessionCookieAsync()` method for diff --git a/pom.xml b/pom.xml index d750657ff..7b15a89ad 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 5.10.1-SNAPSHOT + 5.11.1-SNAPSHOT jar firebase-admin From 93d081011f7a0a2a558d401db6cff4eeb577dc55 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 7 May 2018 14:05:30 -0700 Subject: [PATCH 010/456] Introducing Synchronous Methods to Public APIs (#146) * Adding the sync APIs for FirebaseAuth * Added more tests; Added sync APIs for FirebaseMessaging * Removing Task references from database, iid and fcm APIs * Fixing a typo * Minor code clean up * Updated javadocs; Renamed internal helpers of FirebaseMessaging for consistency * Removed the deprecated FirebaseCredential API (#149) * Removing the Task API (#152) * Removed the deprecated FirebaseCredential API * Removing the deprecated Task API * Dropping Support for App Engine Java 7 Runtime (#153) * Dropping support for GAE 7 * Removed GaeThreadFactory, GaeExecutorService and RevivingScheduledExecutor * Removed the deprecated FirebaseCredential API * Removing GAE java7 related APIs (GaeThreadFactory, RevivingScheduledExecutor) * Removed GaePlatform implementation * Added FirebaseScheduledExecutor * Updated documentation * Some minor nits from code reviews * Calling super method in DefaultRunLoop executor * Removing Deprecated LogWrapper API (#154) * Dropping support for GAE 7 * Removed GaeThreadFactory, GaeExecutorService and RevivingScheduledExecutor * Removed the deprecated FirebaseCredential API * Removing GAE java7 related APIs (GaeThreadFactory, RevivingScheduledExecutor) * Removed GaePlatform implementation * Added FirebaseScheduledExecutor * Updated documentation * Removing LogWrapper API * Removing PrefixedLogger * Removing test config file * Updated CHANGELOG * Minor clean ups pointed out in the code review * Minor API doc fixes (#171) --- CHANGELOG.md | 12 + .../java/com/google/firebase/FirebaseApp.java | 98 +-- .../com/google/firebase/FirebaseOptions.java | 44 +- .../firebase/ImplFirebaseTrampolines.java | 13 +- .../com/google/firebase/ThreadManager.java | 2 +- .../google/firebase/auth/FirebaseAuth.java | 727 +++++++++++------- .../firebase/auth/FirebaseCredential.java | 39 - .../firebase/auth/FirebaseCredentials.java | 223 ------ .../firebase/auth/GoogleOAuthAccessToken.java | 62 -- .../auth/internal/BaseCredential.java | 77 -- .../internal/FirebaseCredentialsAdapter.java | 51 -- .../firebase/database/ChildEventListener.java | 2 +- .../firebase/database/DataSnapshot.java | 8 +- .../firebase/database/DatabaseReference.java | 74 +- .../firebase/database/FirebaseDatabase.java | 18 - .../google/firebase/database/MutableData.java | 4 +- .../firebase/database/OnDisconnect.java | 92 +-- .../database/connection/Connection.java | 86 +-- .../connection/ConnectionContext.java | 9 - .../connection/NettyWebSocketClient.java | 11 +- .../connection/PersistentConnectionImpl.java | 180 ++--- .../connection/WebsocketConnection.java | 60 +- .../database/connection/util/RetryHelper.java | 17 +- .../firebase/database/core/Context.java | 77 +- .../database/core/DatabaseConfig.java | 63 -- .../firebase/database/core/GaePlatform.java | 131 ---- .../firebase/database/core/JvmPlatform.java | 22 +- .../firebase/database/core/Platform.java | 6 - .../google/firebase/database/core/Repo.java | 80 +- .../firebase/database/core/SyncTree.java | 12 +- .../database/core/ThreadInitializer.java | 46 -- .../database/core/ThreadPoolEventTarget.java | 30 +- .../DefaultPersistenceManager.java | 26 +- .../core/persistence/TrackedQueryManager.java | 19 +- .../database/core/view/EventRaiser.java | 15 +- .../database/logging/DefaultLogger.java | 93 --- .../firebase/database/logging/LogWrapper.java | 127 --- .../firebase/database/logging/Logger.java | 47 -- .../database/utilities/DefaultRunLoop.java | 60 +- .../database/utilities/Utilities.java | 14 +- .../firebase/iid/FirebaseInstanceId.java | 50 +- .../firebase/internal/CallableOperation.java | 49 ++ .../internal/FirebaseScheduledExecutor.java | 60 ++ .../internal/FirebaseThreadManagers.java | 69 +- .../firebase/internal/GaeExecutorService.java | 195 ----- .../firebase/internal/GaeThreadFactory.java | 165 ---- .../ListenableFuture2ApiFuture.java} | 17 +- .../internal/RevivingScheduledExecutor.java | 213 ----- .../firebase/internal/TaskToApiFuture.java | 82 -- .../firebase/messaging/FirebaseMessaging.java | 230 +++--- .../google/firebase/tasks/Continuation.java | 77 -- .../tasks/ContinueWithCompletionListener.java | 71 -- .../ContinueWithTaskCompletionListener.java | 90 --- .../tasks/OnCompleteCompletionListener.java | 67 -- .../firebase/tasks/OnCompleteListener.java | 35 - .../tasks/OnFailureCompletionListener.java | 69 -- .../firebase/tasks/OnFailureListener.java | 34 - .../tasks/OnSuccessCompletionListener.java | 69 -- .../firebase/tasks/OnSuccessListener.java | 33 - .../java/com/google/firebase/tasks/Task.java | 214 ------ .../tasks/TaskCompletionListener.java | 31 - .../tasks/TaskCompletionListenerQueue.java | 84 -- .../firebase/tasks/TaskCompletionSource.java | 72 -- .../google/firebase/tasks/TaskExecutors.java | 64 -- .../com/google/firebase/tasks/TaskImpl.java | 244 ------ .../java/com/google/firebase/tasks/Tasks.java | 264 ------- .../com/google/firebase/FirebaseAppTest.java | 17 +- .../google/firebase/FirebaseOptionsTest.java | 35 +- .../firebase/auth/FirebaseAuthTest.java | 46 +- .../auth/FirebaseCredentialsTest.java | 292 ------- .../auth/FirebaseUserManagerTest.java | 2 +- .../google/firebase/database/TestHelpers.java | 13 +- .../database/connection/ConnectionTest.java | 2 - .../database/core/GaePlatformTest.java | 95 --- .../firebase/database/core/SyncPointTest.java | 21 +- .../DefaultPersistenceManagerTest.java | 4 +- .../persistence/RandomPersistenceTest.java | 4 +- .../persistence/TrackedQueryManagerTest.java | 9 +- .../database/integration/ShutdownExample.java | 2 - .../utilities/DefaultRunLoopTest.java | 6 +- .../database/utilities/UtilitiesTest.java | 36 +- .../firebase/iid/FirebaseInstanceIdTest.java | 42 +- .../internal/CallableOperationTest.java | 106 +++ .../internal/FirebaseThreadManagersTest.java | 6 +- .../internal/GaeExecutorServiceTest.java | 129 ---- .../RevivingScheduledExecutorTest.java | 238 ------ .../internal/TaskToApiFutureTest.java | 81 -- .../messaging/FirebaseMessagingTest.java | 165 +++- .../OnCompleteCompletionListenerTest.java | 46 -- .../OnFailureCompletionListenerTest.java | 59 -- .../OnSuccessCompletionListenerTest.java | 59 -- .../tasks/TaskCompletionSourceTest.java | 103 --- .../firebase/tasks/TaskExecutorsTest.java | 55 -- .../google/firebase/tasks/TaskImplTest.java | 600 --------------- .../com/google/firebase/tasks/TasksTest.java | 315 -------- .../tasks/testing/TestOnCompleteListener.java | 67 -- .../tasks/testing/TestOnFailureListener.java | 66 -- .../tasks/testing/TestOnSuccessListener.java | 65 -- .../firebase/testing/GenericFunction.java} | 10 +- 99 files changed, 1388 insertions(+), 6833 deletions(-) delete mode 100644 src/main/java/com/google/firebase/auth/FirebaseCredential.java delete mode 100644 src/main/java/com/google/firebase/auth/FirebaseCredentials.java delete mode 100644 src/main/java/com/google/firebase/auth/GoogleOAuthAccessToken.java delete mode 100644 src/main/java/com/google/firebase/auth/internal/BaseCredential.java delete mode 100644 src/main/java/com/google/firebase/auth/internal/FirebaseCredentialsAdapter.java delete mode 100644 src/main/java/com/google/firebase/database/core/GaePlatform.java delete mode 100644 src/main/java/com/google/firebase/database/core/ThreadInitializer.java delete mode 100644 src/main/java/com/google/firebase/database/logging/DefaultLogger.java delete mode 100644 src/main/java/com/google/firebase/database/logging/LogWrapper.java delete mode 100644 src/main/java/com/google/firebase/database/logging/Logger.java create mode 100644 src/main/java/com/google/firebase/internal/CallableOperation.java create mode 100644 src/main/java/com/google/firebase/internal/FirebaseScheduledExecutor.java delete mode 100644 src/main/java/com/google/firebase/internal/GaeExecutorService.java delete mode 100644 src/main/java/com/google/firebase/internal/GaeThreadFactory.java rename src/main/java/com/google/firebase/{tasks/RuntimeExecutionException.java => internal/ListenableFuture2ApiFuture.java} (54%) delete mode 100644 src/main/java/com/google/firebase/internal/RevivingScheduledExecutor.java delete mode 100644 src/main/java/com/google/firebase/internal/TaskToApiFuture.java delete mode 100644 src/main/java/com/google/firebase/tasks/Continuation.java delete mode 100644 src/main/java/com/google/firebase/tasks/ContinueWithCompletionListener.java delete mode 100644 src/main/java/com/google/firebase/tasks/ContinueWithTaskCompletionListener.java delete mode 100644 src/main/java/com/google/firebase/tasks/OnCompleteCompletionListener.java delete mode 100644 src/main/java/com/google/firebase/tasks/OnCompleteListener.java delete mode 100644 src/main/java/com/google/firebase/tasks/OnFailureCompletionListener.java delete mode 100644 src/main/java/com/google/firebase/tasks/OnFailureListener.java delete mode 100644 src/main/java/com/google/firebase/tasks/OnSuccessCompletionListener.java delete mode 100644 src/main/java/com/google/firebase/tasks/OnSuccessListener.java delete mode 100644 src/main/java/com/google/firebase/tasks/Task.java delete mode 100644 src/main/java/com/google/firebase/tasks/TaskCompletionListener.java delete mode 100644 src/main/java/com/google/firebase/tasks/TaskCompletionListenerQueue.java delete mode 100644 src/main/java/com/google/firebase/tasks/TaskCompletionSource.java delete mode 100644 src/main/java/com/google/firebase/tasks/TaskExecutors.java delete mode 100644 src/main/java/com/google/firebase/tasks/TaskImpl.java delete mode 100644 src/main/java/com/google/firebase/tasks/Tasks.java delete mode 100644 src/test/java/com/google/firebase/auth/FirebaseCredentialsTest.java delete mode 100644 src/test/java/com/google/firebase/database/core/GaePlatformTest.java create mode 100644 src/test/java/com/google/firebase/internal/CallableOperationTest.java delete mode 100644 src/test/java/com/google/firebase/internal/GaeExecutorServiceTest.java delete mode 100644 src/test/java/com/google/firebase/internal/RevivingScheduledExecutorTest.java delete mode 100644 src/test/java/com/google/firebase/internal/TaskToApiFutureTest.java delete mode 100644 src/test/java/com/google/firebase/tasks/OnCompleteCompletionListenerTest.java delete mode 100644 src/test/java/com/google/firebase/tasks/OnFailureCompletionListenerTest.java delete mode 100644 src/test/java/com/google/firebase/tasks/OnSuccessCompletionListenerTest.java delete mode 100644 src/test/java/com/google/firebase/tasks/TaskCompletionSourceTest.java delete mode 100644 src/test/java/com/google/firebase/tasks/TaskExecutorsTest.java delete mode 100644 src/test/java/com/google/firebase/tasks/TaskImplTest.java delete mode 100644 src/test/java/com/google/firebase/tasks/TasksTest.java delete mode 100644 src/test/java/com/google/firebase/tasks/testing/TestOnCompleteListener.java delete mode 100644 src/test/java/com/google/firebase/tasks/testing/TestOnFailureListener.java delete mode 100644 src/test/java/com/google/firebase/tasks/testing/TestOnSuccessListener.java rename src/{main/java/com/google/firebase/database/logging/package-info.java => test/java/com/google/firebase/testing/GenericFunction.java} (75%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9040ff20a..eba2e178f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Unreleased +- [added] `FirebaseAuth`, `FirebaseMessaging` and `FirebaseInstanceId` + interfaces now expose a set of blocking methods. Each operation has + blocking an asynchronous versions. +- [changed] Removed the deprecated `FirebaseCredential` interface. +- [changed] Removed the deprecated `Task` interface along with the + `com.google.firebase.tasks` package. +- [changed] Dropped support for App Engine's Java 7 runtime. Developers + are advised to use the Admin SDK with Java 8 when deploying to App + Engine. +- [changed] Removed the deprecated `FirebaseDatabase.setLogLevel()` API + and the related logging utilities. Developers should use SLF4J to + configure logging directly. # v5.11.0 diff --git a/src/main/java/com/google/firebase/FirebaseApp.java b/src/main/java/com/google/firebase/FirebaseApp.java index 52cffb15c..6c6710123 100644 --- a/src/main/java/com/google/firebase/FirebaseApp.java +++ b/src/main/java/com/google/firebase/FirebaseApp.java @@ -19,11 +19,11 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; -import static java.nio.charset.StandardCharsets.UTF_8; import com.google.api.client.googleapis.util.Utils; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonParser; +import com.google.api.core.ApiFuture; import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.OAuth2Credentials; @@ -34,18 +34,13 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; -import com.google.common.io.BaseEncoding; import com.google.firebase.internal.FirebaseAppStore; +import com.google.firebase.internal.FirebaseScheduledExecutor; import com.google.firebase.internal.FirebaseService; -import com.google.firebase.internal.GaeThreadFactory; +import com.google.firebase.internal.ListenableFuture2ApiFuture; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; -import com.google.firebase.internal.RevivingScheduledExecutor; -import com.google.firebase.tasks.Task; -import com.google.firebase.tasks.Tasks; - -import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; @@ -176,6 +171,9 @@ public static FirebaseApp getInstance(@NonNull String name) { * by looking up the {@code FIREBASE_CONFIG} environment variable. If the value of * the variable starts with '{', it is parsed as a JSON object. Otherwise it is * treated as a file name and the JSON content is read from the corresponding file. + * + * @throws IllegalStateException if the default app has already been initialized. + * @throws IllegalArgumentException if an error occurs while loading options from the environment. */ public static FirebaseApp initializeApp() { return initializeApp(DEFAULT_APP_NAME); @@ -185,13 +183,23 @@ public static FirebaseApp initializeApp() { * Initializes a named {@link FirebaseApp} instance using Google Application Default Credentials. * Loads additional {@link FirebaseOptions} from the environment in the same way as the * {@link #initializeApp()} method. + * + * @throws IllegalStateException if an app with the same name has already been initialized. + * @throws IllegalArgumentException if an error occurs while loading options from the environment. */ public static FirebaseApp initializeApp(String name) { - return initializeApp(getOptionsFromEnvironment(), name); + try { + return initializeApp(getOptionsFromEnvironment(), name); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to load settings from the system's environment variables", e); + } } /** * Initializes the default {@link FirebaseApp} instance using the given options. + * + * @throws IllegalStateException if the default app has already been initialized. */ public static FirebaseApp initializeApp(FirebaseOptions options) { return initializeApp(options, DEFAULT_APP_NAME); @@ -241,19 +249,6 @@ static void clearInstancesForTest() { } } - /** - * Returns persistence key. Exists to support getting {@link FirebaseApp} persistence key after - * the app has been deleted. - */ - static String getPersistenceKey(String name, FirebaseOptions options) { - return BaseEncoding.base64Url().omitPadding().encode(name.getBytes(UTF_8)); - } - - /** Use this key to store data per FirebaseApp. */ - String getPersistenceKey() { - return FirebaseApp.getPersistenceKey(getName(), getOptions()); - } - private static List getAllAppNames() { Set allAppNames = new HashSet<>(); synchronized (appsLock) { @@ -317,10 +312,7 @@ String getProjectId() { @Override public boolean equals(Object o) { - if (!(o instanceof FirebaseApp)) { - return false; - } - return name.equals(((FirebaseApp) o).getName()); + return o instanceof FirebaseApp && name.equals(((FirebaseApp) o).getName()); } @Override @@ -383,8 +375,8 @@ private ScheduledExecutorService ensureScheduledExecutorService() { synchronized (lock) { checkNotDeleted(); if (scheduledExecutor == null) { - scheduledExecutor = new RevivingScheduledExecutor(threadManager.getThreadFactory(), - "firebase-scheduled-worker", GaeThreadFactory.isAvailable()); + scheduledExecutor = new FirebaseScheduledExecutor(getThreadFactory(), + "firebase-scheduled-worker"); } } } @@ -395,10 +387,9 @@ ThreadFactory getThreadFactory() { return threadManager.getThreadFactory(); } - // TODO: Return an ApiFuture once Task API is fully removed. - Task submit(Callable command) { + ApiFuture submit(Callable command) { checkNotNull(command); - return Tasks.call(executors.getListeningExecutor(), command); + return new ListenableFuture2ApiFuture<>(executors.getListeningExecutor().submit(command)); } ScheduledFuture schedule(Callable command, long delayMillis) { @@ -462,7 +453,7 @@ static class TokenRefresher implements CredentialsChangedListener { } @Override - public final synchronized void onChanged(OAuth2Credentials credentials) throws IOException { + public final synchronized void onChanged(OAuth2Credentials credentials) { if (state.get() != State.STARTED) { return; } @@ -569,33 +560,26 @@ enum State { } } - private static FirebaseOptions getOptionsFromEnvironment() { + private static FirebaseOptions getOptionsFromEnvironment() throws IOException { String defaultConfig = System.getenv(FIREBASE_CONFIG_ENV_VAR); - try { - if (Strings.isNullOrEmpty(defaultConfig)) { - return new FirebaseOptions.Builder() - .setCredentials(GoogleCredentials.getApplicationDefault()) - .build(); - } - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - FirebaseOptions.Builder builder = new FirebaseOptions.Builder(); - JsonParser parser; - if (defaultConfig.startsWith("{")) { - parser = jsonFactory.createJsonParser(defaultConfig); - } else { - FileReader reader; - reader = new FileReader(defaultConfig); - parser = jsonFactory.createJsonParser(reader); - } - parser.parseAndClose(builder); - builder.setCredentials(GoogleCredentials.getApplicationDefault()); - - return builder.build(); + if (Strings.isNullOrEmpty(defaultConfig)) { + return new FirebaseOptions.Builder() + .setCredentials(GoogleCredentials.getApplicationDefault()) + .build(); + } - } catch (FileNotFoundException e) { - throw new IllegalStateException(e); - } catch (IOException e) { - throw new IllegalStateException(e); + JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + FirebaseOptions.Builder builder = new FirebaseOptions.Builder(); + JsonParser parser; + if (defaultConfig.startsWith("{")) { + parser = jsonFactory.createJsonParser(defaultConfig); + } else { + FileReader reader; + reader = new FileReader(defaultConfig); + parser = jsonFactory.createJsonParser(reader); } + parser.parseAndClose(builder); + builder.setCredentials(GoogleCredentials.getApplicationDefault()); + return builder.build(); } } diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java index 28c9e9e66..0cdf87bf8 100644 --- a/src/main/java/com/google/firebase/FirebaseOptions.java +++ b/src/main/java/com/google/firebase/FirebaseOptions.java @@ -25,21 +25,35 @@ import com.google.api.client.util.Key; import com.google.auth.oauth2.GoogleCredentials; import com.google.common.base.Strings; -import com.google.firebase.auth.FirebaseCredential; -import com.google.firebase.auth.FirebaseCredentials; -import com.google.firebase.auth.internal.BaseCredential; -import com.google.firebase.auth.internal.FirebaseCredentialsAdapter; +import com.google.common.collect.ImmutableList; import com.google.firebase.internal.FirebaseThreadManagers; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; import java.util.HashMap; +import java.util.List; import java.util.Map; /** Configurable Firebase options. */ public final class FirebaseOptions { - // TODO: deprecate and remove it once we can fetch these from Remote Config. + private static final List FIREBASE_SCOPES = + ImmutableList.of( + // Enables access to Firebase Realtime Database. + "https://www.googleapis.com/auth/firebase.database", + + // Enables access to the email address associated with a project. + "https://www.googleapis.com/auth/userinfo.email", + + // Enables access to Google Identity Toolkit (for user management APIs). + "https://www.googleapis.com/auth/identitytoolkit", + + // Enables access to Google Cloud Storage. + "https://www.googleapis.com/auth/devstorage.full_control", + + // Enables access to Google Cloud Firestore + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/datastore"); private final String databaseUrl; private final String storageBucket; @@ -55,7 +69,7 @@ public final class FirebaseOptions { private FirebaseOptions(@NonNull FirebaseOptions.Builder builder) { this.credentials = checkNotNull(builder.credentials, "FirebaseOptions must be initialized with setCredentials().") - .createScoped(BaseCredential.FIREBASE_SCOPES); + .createScoped(FIREBASE_SCOPES); this.databaseUrl = builder.databaseUrl; this.databaseAuthVariableOverride = builder.databaseAuthVariableOverride; this.projectId = builder.projectId; @@ -260,24 +274,6 @@ public Builder setCredentials(GoogleCredentials credentials) { return this; } - /** - * Sets the FirebaseCredential to use to authenticate the SDK. - * - * @param credential A FirebaseCredential used to authenticate the SDK. See {@link - * FirebaseCredentials} for default implementations. - * @return This Builder instance is returned so subsequent calls can be chained. - * @deprecated Use {@link FirebaseOptions.Builder#setCredentials(GoogleCredentials)}. - */ - public Builder setCredential(@NonNull FirebaseCredential credential) { - checkNotNull(credential); - if (credential instanceof BaseCredential) { - this.credentials = ((BaseCredential) credential).getGoogleCredentials(); - } else { - this.credentials = new FirebaseCredentialsAdapter(credential); - } - return this; - } - /** * Sets the auth variable to be used by the Realtime Database rules. * diff --git a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java index 7ffac6d11..5c173ae28 100644 --- a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java +++ b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java @@ -16,11 +16,11 @@ package com.google.firebase; +import com.google.api.core.ApiFuture; import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; -import com.google.firebase.tasks.Task; import java.util.concurrent.Callable; import java.util.concurrent.ThreadFactory; @@ -47,14 +47,6 @@ public static boolean isDefaultApp(@NonNull FirebaseApp app) { return app.isDefaultApp(); } - public static String getPersistenceKey(@NonNull FirebaseApp app) { - return app.getPersistenceKey(); - } - - public static String getPersistenceKey(String name, FirebaseOptions options) { - return FirebaseApp.getPersistenceKey(name, options); - } - public static T getService( @NonNull FirebaseApp app, @NonNull String id, @NonNull Class type) { return type.cast(app.getService(id)); @@ -70,7 +62,8 @@ public static ThreadFactory getThreadFactory(@NonNull FirebaseApp app) { return app.getThreadFactory(); } - public static Task submitCallable(@NonNull FirebaseApp app, @NonNull Callable command) { + public static ApiFuture submitCallable( + @NonNull FirebaseApp app, @NonNull Callable command) { return app.submit(command); } diff --git a/src/main/java/com/google/firebase/ThreadManager.java b/src/main/java/com/google/firebase/ThreadManager.java index 4f3c5f29d..8903101f6 100644 --- a/src/main/java/com/google/firebase/ThreadManager.java +++ b/src/main/java/com/google/firebase/ThreadManager.java @@ -57,7 +57,7 @@ final void releaseFirebaseExecutors( * {@link #getThreadFactory()} method. * * @param app A {@link FirebaseApp} instance. - * @return A non-null {@link ExecutorService} instance. + * @return A non-null ExecutorService instance. */ @NonNull protected abstract ExecutorService getExecutor(@NonNull FirebaseApp app); diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 3150b1d75..3181dc5b1 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -36,13 +36,13 @@ import com.google.firebase.auth.internal.FirebaseTokenFactory; import com.google.firebase.auth.internal.FirebaseTokenVerifier; import com.google.firebase.auth.internal.KeyManagers; +import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; -import com.google.firebase.internal.TaskToApiFuture; -import com.google.firebase.tasks.Task; +import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.Map; -import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -55,6 +55,10 @@ */ public class FirebaseAuth { + private static final String ERROR_CUSTOM_TOKEN = "ERROR_CUSTOM_TOKEN"; + private static final String ERROR_INVALID_ID_TOKEN = "ERROR_INVALID_CREDENTIAL"; + private static final String ERROR_INVALID_SESSION_COOKIE = "ERROR_INVALID_COOKIE"; + private final Clock clock; private final FirebaseApp firebaseApp; @@ -111,25 +115,27 @@ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { return service.getInstance(); } - private Task createSessionCookie( - final String idToken, final SessionCookieOptions options) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty"); - checkNotNull(options, "options must not be null"); - return call(new Callable() { - @Override - public String call() throws Exception { - return userManager.createSessionCookie(idToken, options); - } - }); - } - /** * Creates a new Firebase session cookie from the given ID token and options. The returned JWT * can be set as a server-side session cookie with a custom cookie policy. * * @param idToken The Firebase ID token to exchange for a session cookie. * @param options Additional options required to create the cookie. + * @return A Firebase session cookie string. + * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. + * @throws FirebaseAuthException If an error occurs while generating the session cookie. + */ + public String createSessionCookie( + @NonNull String idToken, @NonNull SessionCookieOptions options) throws FirebaseAuthException { + return createSessionCookieOp(idToken, options).call(); + } + + /** + * Similar to {@link #createSessionCookie(String, SessionCookieOptions)} but performs the + * operation asynchronously. + * + * @param idToken The Firebase ID token to exchange for a session cookie. + * @param options Additional options required to create the cookie. * @return An {@code ApiFuture} which will complete successfully with a session cookie string. * If an error occurs while generating the cookie or if the specified ID token is invalid, * the future throws a {@link FirebaseAuthException}. @@ -137,25 +143,36 @@ public String call() throws Exception { */ public ApiFuture createSessionCookieAsync( @NonNull String idToken, @NonNull SessionCookieOptions options) { - return new TaskToApiFuture<>(createSessionCookie(idToken, options)); + return createSessionCookieOp(idToken, options).callAsync(firebaseApp); + } + + private CallableOperation createSessionCookieOp( + final String idToken, final SessionCookieOptions options) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty"); + checkNotNull(options, "options must not be null"); + return new CallableOperation() { + @Override + protected String execute() throws FirebaseAuthException { + return userManager.createSessionCookie(idToken, options); + } + }; } /** * Parses and verifies a Firebase session cookie. * - *

If verified successfully, the returned {@code ApiFuture} completes with a parsed version of - * the cookie from which the UID and the other claims can be read. If the cookie is invalid, - * the future throws an exception indicating the failure. + *

If verified successfully, returns a parsed version of the cookie from which the UID and the + * other claims can be read. If the cookie is invalid, throws a {@link FirebaseAuthException}. * *

This method does not check whether the cookie has been revoked. See - * {@link #verifySessionCookieAsync(String, boolean)}. + * {@link #verifySessionCookie(String, boolean)}. * * @param cookie A Firebase session cookie string to verify and parse. - * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or - * unsuccessfully with the failure Exception. + * @return A {@link FirebaseToken} representing the verified and decoded cookie. */ - public ApiFuture verifySessionCookieAsync(String cookie) { - return new TaskToApiFuture<>(verifySessionCookie(cookie, false)); + public FirebaseToken verifySessionCookie(String cookie) throws FirebaseAuthException { + return verifySessionCookie(cookie, false); } /** @@ -164,10 +181,34 @@ public ApiFuture verifySessionCookieAsync(String cookie) { *

If {@code checkRevoked} is true, additionally verifies that the cookie has not been * revoked. * - *

If verified successfully, the returned {@code ApiFuture} completes with a parsed version of - * the cookie from which the UID and the other claims can be read. If the cookie is invalid or - * has been revoked while {@code checkRevoked} is true, the future throws an exception indicating - * the failure. + *

If verified successfully, returns a parsed version of the cookie from which the UID and the + * other claims can be read. If the cookie is invalid or has been revoked while + * {@code checkRevoked} is true, throws a {@link FirebaseAuthException}. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly + * revoked. + * @return A {@link FirebaseToken} representing the verified and decoded cookie. + */ + public FirebaseToken verifySessionCookie( + String cookie, boolean checkRevoked) throws FirebaseAuthException { + return verifySessionCookieOp(cookie, checkRevoked).call(); + } + + /** + * Similar to {@link #verifySessionCookie(String)} but performs the operation asynchronously. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or + * unsuccessfully with the failure Exception. + */ + public ApiFuture verifySessionCookieAsync(String cookie) { + return verifySessionCookieAsync(cookie, false); + } + + /** + * Similar to {@link #verifySessionCookie(String, boolean)} but performs the operation + * asynchronously. * * @param cookie A Firebase session cookie string to verify and parse. * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly @@ -176,19 +217,26 @@ public ApiFuture verifySessionCookieAsync(String cookie) { * unsuccessfully with the failure Exception. */ public ApiFuture verifySessionCookieAsync(String cookie, boolean checkRevoked) { - return new TaskToApiFuture<>(verifySessionCookie(cookie, checkRevoked)); + return verifySessionCookieOp(cookie, checkRevoked).callAsync(firebaseApp); } - private Task verifySessionCookie(final String cookie, final boolean checkRevoked) { + private CallableOperation verifySessionCookieOp( + final String cookie, final boolean checkRevoked) { checkNotDestroyed(); checkState(!Strings.isNullOrEmpty(projectId), "Must initialize FirebaseApp with a project ID to call verifySessionCookie()"); - return call(new Callable() { + return new CallableOperation() { @Override - public FirebaseToken call() throws Exception { + public FirebaseToken execute() throws FirebaseAuthException { FirebaseTokenVerifier firebaseTokenVerifier = FirebaseTokenVerifier.createSessionCookieVerifier(projectId, keyManagers, clock); - FirebaseToken firebaseToken = FirebaseToken.parse(jsonFactory, cookie); + FirebaseToken firebaseToken; + try { + firebaseToken = FirebaseToken.parse(jsonFactory, cookie); + } catch (IOException e) { + throw new FirebaseAuthException(ERROR_INVALID_SESSION_COOKIE, + "Failed to parse cookie", e); + } // This will throw a FirebaseAuthException with details on how the token is invalid. firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken()); @@ -198,7 +246,7 @@ public FirebaseToken call() throws Exception { } return firebaseToken; } - }); + }; } private void checkRevoked( @@ -212,106 +260,202 @@ private void checkRevoked( } /** - * Similar to {@link #createCustomTokenAsync(String)}, but returns a {@link Task}. + * Creates a Firebase custom token for the given UID. This token can then be sent back to a client + * application to be used with the + * signInWithCustomToken + * authentication API. + * + *

{@link FirebaseApp} must have been initialized with service account credentials to use + * call this method. * * @param uid The UID to store in the token. This identifies the user to other Firebase services - * (Firebase Database, Firebase Auth, etc.) - * @return A {@link Task} which will complete successfully with the created Firebase Custom Token, - * or unsuccessfully with the failure Exception. - * @deprecated Use {@link #createCustomTokenAsync(String)} + * (Realtime Database, Firebase Auth, etc.). Should be less than 128 characters. + * @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 FirebaseAuthException If an error occurs while generating the custom token. */ - public Task createCustomToken(String uid) { + public String createCustomToken(@NonNull String uid) throws FirebaseAuthException { return createCustomToken(uid, null); } /** - * Similar to {@link #createCustomTokenAsync(String, Map)}, but returns a {@link Task}. + * Creates a Firebase custom token for the given UID, containing the specified additional + * claims. This token can then be sent back to a client application to be used with the + * signInWithCustomToken + * authentication API. + * + *

{@link FirebaseApp} must have been initialized with service account credentials to use + * call this method. * * @param uid The UID to store in the token. This identifies the user to other Firebase services - * (Realtime Database, Storage, etc.). Should be less than 128 characters. + * (Realtime Database, Firebase Auth, etc.). Should be less than 128 characters. * @param developerClaims Additional claims to be stored in the token (and made available to * 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 {@link Task} which will complete successfully with the created Firebase Custom Token, - * or unsuccessfully with the failure Exception. - * @deprecated Use {@link #createCustomTokenAsync(String, Map)} + * @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 FirebaseAuthException If an error occurs while generating the custom token. */ - public Task createCustomToken( - final String uid, final Map developerClaims) { - checkNotDestroyed(); - checkState(credentials instanceof ServiceAccountCredentials, - "Must initialize FirebaseApp with a service account credential to call " - + "createCustomToken()"); - - final ServiceAccountCredentials serviceAccount = (ServiceAccountCredentials) credentials; - return call(new Callable() { - @Override - public String call() throws Exception { - FirebaseTokenFactory tokenFactory = FirebaseTokenFactory.getInstance(); - return tokenFactory.createSignedCustomAuthTokenForUser( - uid, - developerClaims, - serviceAccount.getClientEmail(), - serviceAccount.getPrivateKey()); - } - }); + public String createCustomToken(@NonNull String uid, + @Nullable Map developerClaims) throws FirebaseAuthException { + return createCustomTokenOp(uid, developerClaims).call(); } /** - * Creates a Firebase Custom Token associated with the given UID. This token can then be provided - * back to a client application for use with the - * signInWithCustomToken - * authentication API. + * Similar to {@link #createCustomToken(String)} but performs the operation asynchronously. * * @param uid The UID to store in the token. This identifies the user to other Firebase services - * (Firebase Realtime Database, Firebase Auth, etc.) - * @return An {@code ApiFuture} which will complete successfully with the created Firebase Custom - * Token, or unsuccessfully with the failure Exception. + * (Realtime Database, Firebase Auth, etc.). Should be less than 128 characters. + * @return An {@code ApiFuture} which will complete successfully with the created Firebase custom + * token, or unsuccessfully with the failure Exception. + * @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not + * been initialized with service account credentials. */ - public ApiFuture createCustomTokenAsync(String uid) { - return new TaskToApiFuture<>(createCustomToken(uid)); + public ApiFuture createCustomTokenAsync(@NonNull String uid) { + return createCustomTokenAsync(uid, null); } /** - * Creates a Firebase Custom Token associated with the given UID and additionally containing the - * specified developerClaims. This token can then be provided back to a client application for use - * with the signInWithCustomToken authentication API. + * Similar to {@link #createCustomToken(String, Map)} but performs the operation + * asynchronously. * * @param uid The UID to store in the token. This identifies the user to other Firebase services * (Realtime Database, Storage, etc.). Should be less than 128 characters. * @param developerClaims Additional claims to be stored in the token (and made available to * 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 An {@code ApiFuture} which will complete successfully with the created Firebase Custom - * Token, or unsuccessfully with the failure Exception. + * @return An {@code ApiFuture} which will complete successfully with the created Firebase custom + * token, or unsuccessfully with the failure Exception. + * @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not + * been initialized with service account credentials. */ public ApiFuture createCustomTokenAsync( + @NonNull String uid, @Nullable Map developerClaims) { + return createCustomTokenOp(uid, developerClaims).callAsync(firebaseApp); + } + + private CallableOperation createCustomTokenOp( final String uid, final Map developerClaims) { - return new TaskToApiFuture<>(createCustomToken(uid, 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()"); + 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) { + throw new FirebaseAuthException(ERROR_CUSTOM_TOKEN, + "Failed to generate a custom token", e); + } + } + }; } /** - * Similar to {@link #verifyIdTokenAsync(String)}, but returns a {@link Task}. + * Parses and verifies a Firebase ID Token. * - * @param token A Firebase ID Token to verify and parse. - * @return A {@link Task} which will complete successfully with the parsed token, or - * unsuccessfully with the failure Exception. - * @deprecated Use {@link #verifyIdTokenAsync(String)} + *

A Firebase application can identify itself to a trusted backend server by sending its + * Firebase ID Token (accessible via the {@code getToken} API in the Firebase Authentication + * client) with its requests. The backend server can then use the {@code verifyIdToken()} method + * to verify that the token is valid. This method ensures that the token is correctly signed, + * has not expired, and it was issued to the Firebase project associated with this + * {@link FirebaseAuth} instance. + * + *

This method does not check whether a token has been revoked. Use + * {@link #verifyIdToken(String, boolean)} to perform an additional revocation check. + * + * @param token A Firebase ID token string to parse and verify. + * @return A {@link FirebaseToken} representing the verified and decoded token. + * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} + * instance does not have a project ID associated with it. + * @throws FirebaseAuthException If an error occurs while parsing or validating the token. */ - public Task verifyIdToken(final String token) { + public FirebaseToken verifyIdToken(@NonNull String token) throws FirebaseAuthException { return verifyIdToken(token, false); } - - private Task verifyIdToken(final String token, final boolean checkRevoked) { + + /** + * Parses and verifies a Firebase ID Token. + * + *

A Firebase application can identify itself to a trusted backend server by sending its + * Firebase ID Token (accessible via the {@code getToken} API in the Firebase Authentication + * client) with its requests. The backend server can then use the {@code verifyIdToken()} method + * to verify that the token is valid. This method ensures that the token is correctly signed, + * has not expired, and it was issued to the Firebase project associated with this + * {@link FirebaseAuth} instance. + * + *

If {@code checkRevoked} is set to true, this method performs an additional check to see + * if the ID token has been revoked since it was issues. This requires making an additional + * remote API call. + * + * @param token A Firebase ID token string to parse and verify. + * @param checkRevoked A boolean denoting whether to check if the tokens were revoked. + * @return A {@link FirebaseToken} representing the verified and decoded token. + * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} + * instance does not have a project ID associated with it. + * @throws FirebaseAuthException If an error occurs while parsing or validating the token. + */ + public FirebaseToken verifyIdToken( + @NonNull String token, boolean checkRevoked) throws FirebaseAuthException { + return verifyIdTokenOp(token, checkRevoked).call(); + } + + /** + * Similar to {@link #verifyIdToken(String)} but performs the operation asynchronously. + * + * @param token A Firebase ID Token to verify and parse. + * @return An {@code ApiFuture} which will complete successfully with the parsed token, or + * unsuccessfully with a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} + * instance does not have a project ID associated with it. + */ + public ApiFuture verifyIdTokenAsync(@NonNull String token) { + return verifyIdTokenAsync(token, false); + } + + /** + * Similar to {@link #verifyIdToken(String, boolean)} but performs the operation asynchronously. + * + * @param token A Firebase ID Token to verify and parse. + * @param checkRevoked A boolean denoting whether to check if the tokens were revoked. + * @return An {@code ApiFuture} which will complete successfully with the parsed token, or + * unsuccessfully with a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} + * instance does not have a project ID associated with it. + */ + public ApiFuture verifyIdTokenAsync(@NonNull String token, boolean checkRevoked) { + return verifyIdTokenOp(token, checkRevoked).callAsync(firebaseApp); + } + + private CallableOperation verifyIdTokenOp( + final String token, final boolean checkRevoked) { checkNotDestroyed(); - checkState(!Strings.isNullOrEmpty(projectId), + checkArgument(!Strings.isNullOrEmpty(token), "ID token must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(projectId), "Must initialize FirebaseApp with a project ID to call verifyIdToken()"); - return call(new Callable() { + return new CallableOperation() { @Override - public FirebaseToken call() throws Exception { + protected FirebaseToken execute() throws FirebaseAuthException { FirebaseTokenVerifier firebaseTokenVerifier = FirebaseTokenVerifier.createIdTokenVerifier(projectId, keyManagers, clock); - FirebaseToken firebaseToken = FirebaseToken.parse(jsonFactory, token); + FirebaseToken firebaseToken; + try { + firebaseToken = FirebaseToken.parse(jsonFactory, token); + } catch (IOException e) { + throw new FirebaseAuthException(ERROR_INVALID_ID_TOKEN, "Failed to parse token", e); + } + // This will throw a FirebaseAuthException with details on how the token is invalid. firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken()); @@ -320,122 +464,69 @@ public FirebaseToken call() throws Exception { } return firebaseToken; } - }); - } - - private Task revokeRefreshTokens(String uid) { - checkNotDestroyed(); - long currentTimeSeconds = System.currentTimeMillis() / 1000L; - final UpdateRequest request = new UpdateRequest(uid).setValidSince(currentTimeSeconds); - return call(new Callable() { - @Override - public Void call() throws Exception { - userManager.updateUser(request, jsonFactory); - return null; - } - }); + }; } /** * Revokes all refresh tokens for the specified user. - * + * *

Updates the user's tokensValidAfterTimestamp to the current UTC time expressed in * milliseconds since the epoch and truncated to 1 second accuracy. It is important that the * server on which this is called has its clock set correctly and synchronized. - * + * *

While this will revoke all sessions for a specified user and disable any new ID tokens for * existing sessions from getting minted, existing ID tokens may remain active until their - * natural expiration (one hour). + * natural expiration (one hour). * To verify that ID tokens are revoked, use {@link #verifyIdTokenAsync(String, boolean)}. - * + * * @param uid The user id for which tokens are revoked. - * @return An {@code ApiFuture} which will complete successfully or if updating the user fails, - * unsuccessfully with the failure Exception. + * @throws IllegalArgumentException If the user ID is null or empty. + * @throws FirebaseAuthException If an error occurs while revoking tokens. */ - public ApiFuture revokeRefreshTokensAsync(String uid) { - return new TaskToApiFuture<>(revokeRefreshTokens(uid)); + public void revokeRefreshTokens(@NonNull String uid) throws FirebaseAuthException { + revokeRefreshTokensOp(uid).call(); } /** - * Parses and verifies a Firebase ID Token. - * - *

A Firebase application can identify itself to a trusted backend server by sending its - * Firebase ID Token (accessible via the getToken API in the Firebase Authentication client) with - * its request. - * - *

The backend server can then use the verifyIdToken() method to verify the token is valid, - * meaning: the token is properly signed, has not expired, and it was issued for the project - * associated with this FirebaseAuth instance (which by default is extracted from your service - * account) - * - *

If the token is valid, the returned Future will complete successfully and provide a - * parsed version of the token from which the UID and other claims in the token can be inspected. - * If the token is invalid, the future throws an exception indicating the failure. + * Similar to {@link #revokeRefreshTokens(String)} but performs the operation asynchronously. * - *

This does not check whether a token has been revoked. - * See {@link #verifyIdTokenAsync(String, boolean)} below. - * - * @param token A Firebase ID Token to verify and parse. - * @return An {@code ApiFuture} which will complete successfully with the parsed token, or - * unsuccessfully with the failure Exception. + * @param uid The user id for which tokens are revoked. + * @return An {@code ApiFuture} which will complete successfully or fail with a + * {@link FirebaseAuthException} in the event of an error. + * @throws IllegalArgumentException If the user ID is null or empty. */ - public ApiFuture verifyIdTokenAsync(final String token) { - return verifyIdTokenAsync(token, false); + public ApiFuture revokeRefreshTokensAsync(@NonNull String uid) { + return revokeRefreshTokensOp(uid).callAsync(firebaseApp); } - /** - * Parses and verifies a Firebase ID Token and if requested, checks whether it was revoked. - * - *

A Firebase application can identify itself to a trusted backend server by sending its - * Firebase ID Token (accessible via the getToken API in the Firebase Authentication client) with - * its request. - * - *

The backend server can then use the verifyIdToken() method to verify the token is valid, - * meaning: the token is properly signed, has not expired, and it was issued for the project - * associated with this FirebaseAuth instance (which by default is extracted from your service - * account) - * - *

If {@code checkRevoked} is true, additionally checks if the token has been revoked. - * - *

If the token is valid, and not revoked, the returned Future will complete successfully and - * provide a parsed version of the token from which the UID and other claims in the token can be - * inspected. - * If the token is invalid or has been revoked, the future throws an exception indicating the - * failure. - * - * @param token A Firebase ID Token to verify and parse. - * @param checkRevoked A boolean indicating whether to check if the tokens were revoked. - * @return An {@code ApiFuture} which will complete successfully with the parsed token, or - * unsuccessfully with the failure Exception. - */ - public ApiFuture verifyIdTokenAsync(final String token, - final boolean checkRevoked) { - return new TaskToApiFuture<>(verifyIdToken(token, checkRevoked)); + private CallableOperation revokeRefreshTokensOp(final String uid) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + int currentTimeSeconds = (int) (System.currentTimeMillis() / 1000); + UpdateRequest request = new UpdateRequest(uid).setValidSince(currentTimeSeconds); + userManager.updateUser(request, jsonFactory); + return null; + } + }; } /** - * Similar to {@link #getUserAsync(String)}, but returns a {@link Task}. + * Gets the user data corresponding to the specified user ID. * * @param uid A user ID string. - * @return A {@link Task} which will complete successfully with a {@link UserRecord} instance. - * If an error occurs while retrieving user data or if the specified user ID does not exist, - * the task fails with a {@link FirebaseAuthException}. + * @return A {@link UserRecord} instance. * @throws IllegalArgumentException If the user ID string is null or empty. - * @deprecated Use {@link #getUserAsync(String)} + * @throws FirebaseAuthException If an error occurs while retrieving user data. */ - public Task getUser(final String uid) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - return call(new Callable() { - @Override - public UserRecord call() throws Exception { - return userManager.getUserById(uid); - } - }); + public UserRecord getUser(@NonNull String uid) throws FirebaseAuthException { + return getUserOp(uid).call(); } /** - * Gets the user data corresponding to the specified user ID. + * Similar to {@link #getUser(String)} but performs the operation asynchronously. * * @param uid A user ID string. * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} @@ -443,33 +534,35 @@ public UserRecord call() throws Exception { * not exist, the future throws a {@link FirebaseAuthException}. * @throws IllegalArgumentException If the user ID string is null or empty. */ - public ApiFuture getUserAsync(final String uid) { - return new TaskToApiFuture<>(getUser(uid)); + public ApiFuture getUserAsync(@NonNull String uid) { + return getUserOp(uid).callAsync(firebaseApp); + } + + private CallableOperation getUserOp(final String uid) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + return new CallableOperation() { + @Override + protected UserRecord execute() throws FirebaseAuthException { + return userManager.getUserById(uid); + } + }; } /** - * Similar to {@link #getUserByEmailAsync(String)}, but returns a {@link Task}. + * Gets the user data corresponding to the specified user email. * * @param email A user email address string. - * @return A {@link Task} which will complete successfully with a {@link UserRecord} instance. - * If an error occurs while retrieving user data or if the email address does not correspond - * to a user, the task fails with a {@link FirebaseAuthException}. + * @return A {@link UserRecord} instance. * @throws IllegalArgumentException If the email is null or empty. - * @deprecated Use {@link #getUserByEmailAsync(String)} + * @throws FirebaseAuthException If an error occurs while retrieving user data. */ - public Task getUserByEmail(final String email) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); - return call(new Callable() { - @Override - public UserRecord call() throws Exception { - return userManager.getUserByEmail(email); - } - }); + public UserRecord getUserByEmail(@NonNull String email) throws FirebaseAuthException { + return getUserByEmailOp(email).call(); } /** - * Gets the user data corresponding to the specified user email. + * Similar to {@link #getUserByEmail(String)} but performs the operation asynchronously. * * @param email A user email address string. * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} @@ -477,29 +570,32 @@ public UserRecord call() throws Exception { * correspond to a user, the future throws a {@link FirebaseAuthException}. * @throws IllegalArgumentException If the email is null or empty. */ - public ApiFuture getUserByEmailAsync(final String email) { - return new TaskToApiFuture<>(getUserByEmail(email)); + public ApiFuture getUserByEmailAsync(@NonNull String email) { + return getUserByEmailOp(email).callAsync(firebaseApp); + } + + private CallableOperation getUserByEmailOp( + final String email) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); + return new CallableOperation() { + @Override + protected UserRecord execute() throws FirebaseAuthException { + return userManager.getUserByEmail(email); + } + }; } /** - * Similar to {@link #getUserByPhoneNumberAsync(String)}, but returns a {@link Task}. + * Gets the user data corresponding to the specified user phone number. * * @param phoneNumber A user phone number string. - * @return A {@link Task} which will complete successfully with a {@link UserRecord} instance. - * If an error occurs while retrieving user data or if the phone number does not - * correspond to a user, the task fails with a {@link FirebaseAuthException}. + * @return A a {@link UserRecord} instance. * @throws IllegalArgumentException If the phone number is null or empty. - * @deprecated Use {@link #getUserByPhoneNumberAsync(String)} + * @throws FirebaseAuthException If an error occurs while retrieving user data. */ - public Task getUserByPhoneNumber(final String phoneNumber) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number must not be null or empty"); - return call(new Callable() { - @Override - public UserRecord call() throws Exception { - return userManager.getUserByPhoneNumber(phoneNumber); - } - }); + public UserRecord getUserByPhoneNumber(@NonNull String phoneNumber) throws FirebaseAuthException { + return getUserByPhoneNumberOp(phoneNumber).call(); } /** @@ -511,20 +607,20 @@ public UserRecord call() throws Exception { * correspond to a user, the future throws a {@link FirebaseAuthException}. * @throws IllegalArgumentException If the phone number is null or empty. */ - public ApiFuture getUserByPhoneNumberAsync(final String phoneNumber) { - return new TaskToApiFuture<>(getUserByPhoneNumber(phoneNumber)); + public ApiFuture getUserByPhoneNumberAsync(@NonNull String phoneNumber) { + return getUserByPhoneNumberOp(phoneNumber).callAsync(firebaseApp); } - private Task listUsers(@Nullable String pageToken, int maxResults) { + private CallableOperation getUserByPhoneNumberOp( + final String phoneNumber) { checkNotDestroyed(); - final PageFactory factory = new PageFactory( - new DefaultUserSource(userManager, jsonFactory), maxResults, pageToken); - return call(new Callable() { + checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number must not be null or empty"); + return new CallableOperation() { @Override - public ListUsersPage call() throws Exception { - return factory.create(); + protected UserRecord execute() throws FirebaseAuthException { + return userManager.getUserByPhoneNumber(phoneNumber); } - }); + }; } /** @@ -532,6 +628,34 @@ public ListUsersPage call() throws Exception { * limited to 1000 users. * * @param pageToken A non-empty page token string, or null to retrieve the first page of users. + * @return A {@link ListUsersPage} instance. + * @throws IllegalArgumentException If the specified page token is empty. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public ListUsersPage listUsers(@Nullable String pageToken) throws FirebaseAuthException { + return listUsers(pageToken, FirebaseUserManager.MAX_LIST_USERS_RESULTS); + } + + /** + * Gets a page of users starting from the specified {@code pageToken}. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of users. + * @param maxResults Maximum number of users to include in the returned page. This may not + * exceed 1000. + * @return A {@link ListUsersPage} instance. + * @throws IllegalArgumentException If the specified page token is empty, or max results value + * is invalid. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public ListUsersPage listUsers( + @Nullable String pageToken, int maxResults) throws FirebaseAuthException { + return listUsersOp(pageToken, maxResults).call(); + } + + /** + * Similar to {@link #listUsers(String)} but performs the operation asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of users. * @return An {@code ApiFuture} which will complete successfully with a {@link ListUsersPage} * instance. If an error occurs while retrieving user data, the future throws an exception. * @throws IllegalArgumentException If the specified page token is empty. @@ -541,7 +665,7 @@ public ApiFuture listUsersAsync(@Nullable String pageToken) { } /** - * Gets a page of users starting from the specified {@code pageToken}. + * Similar to {@link #listUsers(String, int)} but performs the operation asynchronously. * * @param pageToken A non-empty page token string, or null to retrieve the first page of users. * @param maxResults Maximum number of users to include in the returned page. This may not @@ -551,30 +675,22 @@ public ApiFuture listUsersAsync(@Nullable String pageToken) { * @throws IllegalArgumentException If the specified page token is empty, or max results value * is invalid. */ - public ApiFuture listUsersAsync(@Nullable String pageToken, int maxResults) { - return new TaskToApiFuture<>(listUsers(pageToken, maxResults)); + public ApiFuture listUsersAsync( + @Nullable final String pageToken, final int maxResults) { + return listUsersOp(pageToken, maxResults).callAsync(firebaseApp); } - /** - * Similar to {@link #createUserAsync(CreateRequest)}, but returns a {@link Task}. - * - * @param request A non-null {@link CreateRequest} instance. - * @return A {@link Task} which will complete successfully with a {@link UserRecord} instance - * corresponding to the newly created account. If an error occurs while creating the user - * account, the task fails with a {@link FirebaseAuthException}. - * @throws NullPointerException if the provided request is null. - * @deprecated Use {@link #createUserAsync(CreateRequest)} - */ - public Task createUser(final CreateRequest request) { + private CallableOperation listUsersOp( + @Nullable String pageToken, int maxResults) { checkNotDestroyed(); - checkNotNull(request, "create request must not be null"); - return call(new Callable() { + final PageFactory factory = new PageFactory( + new DefaultUserSource(userManager, jsonFactory), maxResults, pageToken); + return new CallableOperation() { @Override - public UserRecord call() throws Exception { - String uid = userManager.createUser(request); - return userManager.getUserById(uid); + protected ListUsersPage execute() throws FirebaseAuthException { + return factory.create(); } - }); + }; } /** @@ -582,61 +698,77 @@ public UserRecord call() throws Exception { * {@link CreateRequest}. * * @param request A non-null {@link CreateRequest} instance. + * @return A {@link UserRecord} instance corresponding to the newly created account. + * @throws NullPointerException if the provided request is null. + * @throws FirebaseAuthException if an error occurs while creating the user account. + */ + public UserRecord createUser(@NonNull CreateRequest request) throws FirebaseAuthException { + return createUserOp(request).call(); + } + + /** + * Similar to {@link #createUser(CreateRequest)} but performs the operation asynchronously. + * + * @param request A non-null {@link CreateRequest} instance. * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} * instance corresponding to the newly created account. If an error occurs while creating the * user account, the future throws a {@link FirebaseAuthException}. * @throws NullPointerException if the provided request is null. */ - public ApiFuture createUserAsync(final CreateRequest request) { - return new TaskToApiFuture<>(createUser(request)); + public ApiFuture createUserAsync(@NonNull CreateRequest request) { + return createUserOp(request).callAsync(firebaseApp); + } + + private CallableOperation createUserOp( + final CreateRequest request) { + checkNotDestroyed(); + checkNotNull(request, "create request must not be null"); + return new CallableOperation() { + @Override + protected UserRecord execute() throws FirebaseAuthException { + String uid = userManager.createUser(request); + return userManager.getUserById(uid); + } + }; } /** - * Similar to {@link #updateUserAsync(UpdateRequest)}, but returns a {@link Task}. + * Updates an existing user account with the attributes contained in the specified + * {@link UpdateRequest}. * * @param request A non-null {@link UpdateRequest} instance. - * @return A {@link Task} which will complete successfully with a {@link UserRecord} instance - * corresponding to the updated user account. If an error occurs while updating the user + * @return A {@link UserRecord} instance corresponding to the updated user account. * account, the task fails with a {@link FirebaseAuthException}. * @throws NullPointerException if the provided update request is null. - * @deprecated Use {@link #updateUserAsync(UpdateRequest)} + * @throws FirebaseAuthException if an error occurs while updating the user account. */ - public Task updateUser(final UpdateRequest request) { - checkNotDestroyed(); - checkNotNull(request, "update request must not be null"); - return call(new Callable() { - @Override - public UserRecord call() throws Exception { - userManager.updateUser(request, jsonFactory); - return userManager.getUserById(request.getUid()); - } - }); + public UserRecord updateUser(@NonNull UpdateRequest request) throws FirebaseAuthException { + return updateUserOp(request).call(); } /** - * Updates an existing user account with the attributes contained in the specified - * {@link UpdateRequest}. + * Similar to {@link #updateUser(UpdateRequest)} but performs the operation asynchronously. * * @param request A non-null {@link UpdateRequest} instance. * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} * instance corresponding to the updated user account. If an error occurs while updating the * user account, the future throws a {@link FirebaseAuthException}. - * @throws NullPointerException if the provided update request is null. */ - public ApiFuture updateUserAsync(final UpdateRequest request) { - return new TaskToApiFuture<>(updateUser(request)); + public ApiFuture updateUserAsync(@NonNull UpdateRequest request) { + return updateUserOp(request).callAsync(firebaseApp); } - private Task setCustomClaims(String uid, Map claims) { + private CallableOperation updateUserOp( + final UpdateRequest request) { checkNotDestroyed(); - final UpdateRequest request = new UpdateRequest(uid).setCustomClaims(claims); - return call(new Callable() { + checkNotNull(request, "update request must not be null"); + return new CallableOperation() { @Override - public Void call() throws Exception { + protected UserRecord execute() throws FirebaseAuthException { userManager.updateUser(request, jsonFactory); - return null; + return userManager.getUserById(request.getUid()); } - }); + }; } /** @@ -646,53 +778,78 @@ public Void call() throws Exception { * * @param uid A user ID string. * @param claims A map of custom claims or null. - * @return An {@code ApiFuture} which will complete successfully when the user account has been - * updated. If an error occurs while deleting the user account, the future throws a - * {@link FirebaseAuthException}. + * @throws FirebaseAuthException If an error occurs while updating custom claims. * @throws IllegalArgumentException If the user ID string is null or empty, or the claims * payload is invalid or too large. */ - public ApiFuture setCustomUserClaimsAsync(String uid, Map claims) { - return new TaskToApiFuture<>(setCustomClaims(uid, claims)); + public void setCustomClaims(@NonNull String uid, + @Nullable Map claims) throws FirebaseAuthException { + setCustomUserClaimsOp(uid, claims).call(); } /** - * Similar to {@link #deleteUserAsync(String)}, but returns a {@link Task}. + * Similar to {@link #setCustomClaims(String, Map)} but performs the operation asynchronously. * * @param uid A user ID string. - * @return A {@link Task} which will complete successfully when the specified user account has - * been deleted. If an error occurs while deleting the user account, the task fails with a + * @param claims A map of custom claims or null. + * @return An {@code ApiFuture} which will complete successfully when the user account has been + * updated. If an error occurs while deleting the user account, the future throws a * {@link FirebaseAuthException}. * @throws IllegalArgumentException If the user ID string is null or empty. - * @deprecated Use {@link #deleteUserAsync(String)} */ - public Task deleteUser(final String uid) { + public ApiFuture setCustomUserClaimsAsync( + @NonNull String uid, @Nullable Map claims) { + return setCustomUserClaimsOp(uid, claims).callAsync(firebaseApp); + } + + private CallableOperation setCustomUserClaimsOp( + final String uid, final Map claims) { checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - return call(new Callable() { + return new CallableOperation() { @Override - public Void call() throws Exception { - userManager.deleteUser(uid); + protected Void execute() throws FirebaseAuthException { + final UpdateRequest request = new UpdateRequest(uid).setCustomClaims(claims); + userManager.updateUser(request, jsonFactory); return null; } - }); + }; } /** * Deletes the user identified by the specified user ID. * * @param uid A user ID string. + * @throws IllegalArgumentException If the user ID string is null or empty. + * @throws FirebaseAuthException If an error occurs while deleting the user. + */ + public void deleteUser(@NonNull String uid) throws FirebaseAuthException { + deleteUserOp(uid).call(); + } + + /** + * Similar to {@link #deleteUser(String)} but performs the operation asynchronously. + * + * @param uid A user ID string. * @return An {@code ApiFuture} which will complete successfully when the specified user account * has been deleted. If an error occurs while deleting the user account, the future throws a * {@link FirebaseAuthException}. * @throws IllegalArgumentException If the user ID string is null or empty. */ public ApiFuture deleteUserAsync(final String uid) { - return new TaskToApiFuture<>(deleteUser(uid)); + return deleteUserOp(uid).callAsync(firebaseApp); } - private Task call(Callable command) { - return ImplFirebaseTrampolines.submitCallable(firebaseApp, command); + private CallableOperation deleteUserOp(final String uid) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + userManager.deleteUser(uid); + return null; + } + }; } @VisibleForTesting diff --git a/src/main/java/com/google/firebase/auth/FirebaseCredential.java b/src/main/java/com/google/firebase/auth/FirebaseCredential.java deleted file mode 100644 index b238d299c..000000000 --- a/src/main/java/com/google/firebase/auth/FirebaseCredential.java +++ /dev/null @@ -1,39 +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.auth; - -import com.google.firebase.tasks.Task; - -/** - * Provides Google OAuth2 access tokens used to authenticate with Firebase services. In most cases, - * you will not need to implement this yourself and can instead use the default implementations - * provided by {@link FirebaseCredentials}. - * - * @deprecated Use {@code GoogleCredentials}. - */ -public interface FirebaseCredential { - - /** - * Returns a Google OAuth2 access token which can be used to authenticate with Firebase services. - * This method does not cache tokens, and therefore each invocation will fetch a fresh token. - * The caller is expected to implement caching by referencing the token expiry details - * available in the returned GoogleOAuthAccessToken instance. - * - * @return A {@link Task} providing a Google OAuth access token. - */ - Task getAccessToken(); -} diff --git a/src/main/java/com/google/firebase/auth/FirebaseCredentials.java b/src/main/java/com/google/firebase/auth/FirebaseCredentials.java deleted file mode 100644 index 4ae1d62da..000000000 --- a/src/main/java/com/google/firebase/auth/FirebaseCredentials.java +++ /dev/null @@ -1,223 +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.auth; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -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.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.Strings; -import com.google.firebase.auth.internal.BaseCredential; -import com.google.firebase.internal.NonNull; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Standard {@link FirebaseCredential} implementations for use with {@link - * com.google.firebase.FirebaseOptions}. - * - * @deprecated Use {@code GoogleCredentials}. - */ -public class FirebaseCredentials { - - private FirebaseCredentials() { - } - - /** - * Returns a {@link FirebaseCredential} based on Google Application Default Credentials which can - * be used to authenticate the SDK. - * - *

See Google - * Application Default Credentials for details on Google Application Deafult Credentials. - * - *

See Initialize the SDK for code samples - * and detailed documentation. - * - * @return A {@link FirebaseCredential} based on Google Application Default Credentials which can - * be used to authenticate the SDK. - */ - @NonNull - public static FirebaseCredential applicationDefault() { - return DefaultCredentialsHolder.INSTANCE; - } - - /** - * Returns a {@link FirebaseCredential} based on Google Application Default Credentials which can - * be used to authenticate the SDK. Allows specifying the HttpTransport and the - * JsonFactory to be used when communicating with the remote authentication server. - * - *

See Google - * Application Default Credentials for details on Google Application Deafult Credentials. - * - *

See Initialize the SDK for code samples - * and detailed documentation. - * - * @param transport HttpTransport used to communicate with the remote - * authentication server. - * @param jsonFactory JsonFactory used to parse JSON responses from the remote - * authentication server. - * @return A {@link FirebaseCredential} based on Google Application Default Credentials which can - * be used to authenticate the SDK. - */ - @NonNull - public static FirebaseCredential applicationDefault( - HttpTransport transport, JsonFactory jsonFactory) { - try { - return new ApplicationDefaultCredential(transport); - } catch (IOException e) { - // To prevent a breaking API change, we throw an unchecked exception. - throw new RuntimeException(e); - } - } - - /** - * Returns a {@link FirebaseCredential} generated from the provided service account certificate - * which can be used to authenticate the SDK. - * - *

See Initialize the SDK for code samples - * and detailed documentation. - * - * @param serviceAccount An InputStream containing the JSON representation of a - * service account certificate. - * @return A {@link FirebaseCredential} generated from the provided service account certificate - * which can be used to authenticate the SDK. - * @throws IOException If an error occurs while parsing the service account certificate. - */ - @NonNull - public static FirebaseCredential fromCertificate(InputStream serviceAccount) throws IOException { - return fromCertificate(serviceAccount, Utils.getDefaultTransport(), - Utils.getDefaultJsonFactory()); - } - - /** - * Returns a {@link FirebaseCredential} generated from the provided service account certificate - * which can be used to authenticate the SDK. Allows specifying the HttpTransport - * and the JsonFactory to be used when communicating with the remote authentication - * server. - * - *

See Initialize the SDK for code samples - * and detailed documentation. - * - * @param serviceAccount An InputStream containing the JSON representation of a - * service account certificate. - * @param transport HttpTransport used to communicate with the remote - * authentication server. - * @param jsonFactory JsonFactory used to parse JSON responses from the remote - * authentication server. - * @return A {@link FirebaseCredential} generated from the provided service account certificate - * which can be used to authenticate the SDK. - * @throws IOException If an error occurs while parsing the service account certificate. - */ - @NonNull - public static FirebaseCredential fromCertificate(InputStream serviceAccount, - HttpTransport transport, JsonFactory jsonFactory) throws IOException { - ServiceAccountCredentials credentials = ServiceAccountCredentials.fromStream( - serviceAccount, wrap(transport)); - checkArgument(!Strings.isNullOrEmpty(credentials.getProjectId()), - "Failed to parse service account: 'project_id' must be set"); - return new CertCredential(credentials); - } - - /** - * Returns a {@link FirebaseCredential} generated from the provided refresh token which can be - * used to authenticate the SDK. - * - *

See Initialize the SDK for code samples - * and detailed documentation. - * - * @param refreshToken An InputStream containing the JSON representation of a refresh - * token. - * @return A {@link FirebaseCredential} generated from the provided service account credential - * which can be used to authenticate the SDK. - * @throws IOException If an error occurs while parsing the refresh token. - */ - @NonNull - public static FirebaseCredential fromRefreshToken(InputStream refreshToken) throws IOException { - return fromRefreshToken( - refreshToken, Utils.getDefaultTransport(), Utils.getDefaultJsonFactory()); - } - - /** - * Returns a {@link FirebaseCredential} generated from the provided refresh token which can be - * used to authenticate the SDK. Allows specifying the HttpTransport and the - * JsonFactory to be used when communicating with the remote authentication server. - * - *

See Initialize the SDK for code samples - * and detailed documentation. - * - * @param refreshToken An InputStream containing the JSON representation of a refresh - * token. - * @param transport HttpTransport used to communicate with the remote - * authentication server. - * @param jsonFactory JsonFactory used to parse JSON responses from the remote - * authentication server. - * @return A {@link FirebaseCredential} generated from the provided service account credential - * which can be used to authenticate the SDK. - * @throws IOException If an error occurs while parsing the refresh token. - */ - @NonNull - public static FirebaseCredential fromRefreshToken(final InputStream refreshToken, - HttpTransport transport, JsonFactory jsonFactory) throws IOException { - return new RefreshTokenCredential(refreshToken, transport); - } - - static class CertCredential extends BaseCredential { - - CertCredential(ServiceAccountCredentials credentials) throws IOException { - super(credentials); - } - } - - static class ApplicationDefaultCredential extends BaseCredential { - - ApplicationDefaultCredential(HttpTransport transport) throws IOException { - super(GoogleCredentials.getApplicationDefault(wrap(transport))); - } - } - - static class RefreshTokenCredential extends BaseCredential { - - RefreshTokenCredential(InputStream inputStream, HttpTransport transport) throws IOException { - super(UserCredentials.fromStream(inputStream, wrap(transport))); - } - } - - private static class DefaultCredentialsHolder { - - static final FirebaseCredential INSTANCE = - applicationDefault(Utils.getDefaultTransport(), Utils.getDefaultJsonFactory()); - } - - private static HttpTransportFactory wrap(final HttpTransport transport) { - checkNotNull(transport, "HttpTransport must not be null"); - return new HttpTransportFactory() { - @Override - public HttpTransport create() { - return transport; - } - }; - } -} diff --git a/src/main/java/com/google/firebase/auth/GoogleOAuthAccessToken.java b/src/main/java/com/google/firebase/auth/GoogleOAuthAccessToken.java deleted file mode 100644 index 85e6021e3..000000000 --- a/src/main/java/com/google/firebase/auth/GoogleOAuthAccessToken.java +++ /dev/null @@ -1,62 +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.auth; - -import static com.google.common.base.Preconditions.checkArgument; - -import com.google.common.base.Strings; - -/** - * Represents an OAuth access token, which can be used to access Firebase and other qualified - * Google APIs. Encapsulates both the token string, and its expiration time. - * - * @deprecated Use GoogleCredentials and associated classes. - */ -public class GoogleOAuthAccessToken { - - - private final String accessToken; - private final long expiryTime; - - /** - * Create a new GoogleOAuthAccessToken instance - * - * @param accessToken JWT access token string - * @param expiryTime Time at which the token will expire (milliseconds since epoch) - * @throws IllegalArgumentException If the token is null or empty - */ - public GoogleOAuthAccessToken(String accessToken, long expiryTime) { - checkArgument(!Strings.isNullOrEmpty(accessToken), "Access token must not be null"); - this.accessToken = accessToken; - this.expiryTime = expiryTime; - } - - /** - * Returns the JWT access token. - */ - public String getAccessToken() { - return accessToken; - } - - /** - * Returns the expiration time as a milliseconds since epoch timestamp. - */ - public long getExpiryTime() { - return expiryTime; - } - -} diff --git a/src/main/java/com/google/firebase/auth/internal/BaseCredential.java b/src/main/java/com/google/firebase/auth/internal/BaseCredential.java deleted file mode 100644 index d15832a55..000000000 --- a/src/main/java/com/google/firebase/auth/internal/BaseCredential.java +++ /dev/null @@ -1,77 +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.auth.internal; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.auth.oauth2.AccessToken; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.common.collect.ImmutableList; -import com.google.firebase.auth.FirebaseCredential; -import com.google.firebase.auth.GoogleOAuthAccessToken; -import com.google.firebase.tasks.Task; -import com.google.firebase.tasks.Tasks; - -import java.io.IOException; -import java.util.List; - -/** - * Internal base class for built-in FirebaseCredential implementations. - */ -public abstract class BaseCredential implements FirebaseCredential { - - public static final List FIREBASE_SCOPES = - ImmutableList.of( - // Enables access to Firebase Realtime Database. - "https://www.googleapis.com/auth/firebase.database", - - // Enables access to the email address associated with a project. - "https://www.googleapis.com/auth/userinfo.email", - - // Enables access to Google Identity Toolkit (for user management APIs). - "https://www.googleapis.com/auth/identitytoolkit", - - // Enables access to Google Cloud Storage. - "https://www.googleapis.com/auth/devstorage.full_control", - - // Enables access to Google Cloud Firestore - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/datastore"); - - private final GoogleCredentials googleCredentials; - - public BaseCredential(GoogleCredentials googleCredentials) { - this.googleCredentials = checkNotNull(googleCredentials).createScoped(FIREBASE_SCOPES); - } - - public final GoogleCredentials getGoogleCredentials() { - return googleCredentials; - } - - @Override - public Task getAccessToken() { - try { - AccessToken accessToken = googleCredentials.refreshAccessToken(); - GoogleOAuthAccessToken googleToken = new GoogleOAuthAccessToken(accessToken.getTokenValue(), - accessToken.getExpirationTime().getTime()); - return Tasks.forResult(googleToken); - } catch (Exception e) { - return Tasks.forException(e); - } - } - -} diff --git a/src/main/java/com/google/firebase/auth/internal/FirebaseCredentialsAdapter.java b/src/main/java/com/google/firebase/auth/internal/FirebaseCredentialsAdapter.java deleted file mode 100644 index e1b069eda..000000000 --- a/src/main/java/com/google/firebase/auth/internal/FirebaseCredentialsAdapter.java +++ /dev/null @@ -1,51 +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.auth.internal; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.auth.oauth2.AccessToken; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.firebase.auth.FirebaseCredential; -import com.google.firebase.auth.GoogleOAuthAccessToken; -import com.google.firebase.tasks.Tasks; -import java.io.IOException; -import java.util.Date; -import java.util.concurrent.ExecutionException; - -/** - * An adapter for converting custom {@link FirebaseCredential} implementations into - * GoogleCredentials. - */ -public final class FirebaseCredentialsAdapter extends GoogleCredentials { - - private final FirebaseCredential credential; - - public FirebaseCredentialsAdapter(FirebaseCredential credential) { - this.credential = checkNotNull(credential); - } - - @Override - public AccessToken refreshAccessToken() throws IOException { - try { - GoogleOAuthAccessToken token = Tasks.await(credential.getAccessToken()); - return new AccessToken(token.getAccessToken(), new Date(token.getExpiryTime())); - } catch (ExecutionException | InterruptedException e) { - throw new IOException("Error while obtaining OAuth2 token", e); - } - } -} diff --git a/src/main/java/com/google/firebase/database/ChildEventListener.java b/src/main/java/com/google/firebase/database/ChildEventListener.java index 1a1bb24d1..65bcdfdce 100644 --- a/src/main/java/com/google/firebase/database/ChildEventListener.java +++ b/src/main/java/com/google/firebase/database/ChildEventListener.java @@ -53,7 +53,7 @@ public interface ChildEventListener { /** * This method is triggered when a child location's priority changes. See {@link - * DatabaseReference#setPriority(Object)} and Ordered Data for more information on priorities and ordering data. * diff --git a/src/main/java/com/google/firebase/database/DataSnapshot.java b/src/main/java/com/google/firebase/database/DataSnapshot.java index 700fa2a2a..f1f470505 100644 --- a/src/main/java/com/google/firebase/database/DataSnapshot.java +++ b/src/main/java/com/google/firebase/database/DataSnapshot.java @@ -38,7 +38,7 @@ * They are efficiently-generated immutable copies of the data at a Firebase Database location. They * can't be modified and will never change. To modify data at a location, use a {@link * DatabaseReference DatabaseReference} reference (e.g. with {@link - * DatabaseReference#setValue(Object)}). + * DatabaseReference#setValueAsync(Object)}). */ public class DataSnapshot { @@ -114,7 +114,7 @@ public boolean exists() { *

  • List<Object> * * - *

    This list is recursive; the possible types for {@link java.lang.Object} in the above list + *

    This list is recursive; the possible types for Object in the above list * is given by the same list. These types correspond to the types available in JSON. * * @return The data contained in this snapshot as native types or null if there is no data at this @@ -138,7 +138,7 @@ public Object getValue() { *

  • List<Object> * * - *

    This list is recursive; the possible types for {@link java.lang.Object} in the above list is + *

    This list is recursive; the possible types for Object in the above list is * given by the same list. These types correspond to the types available in JSON. * *

    If useExportFormat is set to true, priority information will be included in the output. @@ -206,7 +206,7 @@ public T getValue(Class valueType) { /** * Due to the way that Java implements generics, it takes an extra step to get back a - * properly-typed Collection. So, in the case where you want a {@link java.util.List} of Message + * properly-typed Collection. So, in the case where you want a List of Message * instances, you will need to do something like the following: * *

    
    diff --git a/src/main/java/com/google/firebase/database/DatabaseReference.java b/src/main/java/com/google/firebase/database/DatabaseReference.java
    index 1c1b9db71..10dfe40ba 100644
    --- a/src/main/java/com/google/firebase/database/DatabaseReference.java
    +++ b/src/main/java/com/google/firebase/database/DatabaseReference.java
    @@ -33,8 +33,6 @@
     import com.google.firebase.database.utilities.Utilities;
     import com.google.firebase.database.utilities.Validation;
     import com.google.firebase.database.utilities.encoding.CustomClassMapper;
    -import com.google.firebase.internal.TaskToApiFuture;
    -import com.google.firebase.tasks.Task;
     
     import java.io.UnsupportedEncodingException;
     import java.net.URLEncoder;
    @@ -177,7 +175,7 @@ public DatabaseReference push() {
        * @return The ApiFuture for this operation.
        */
       public ApiFuture setValueAsync(Object value) {
    -    return new TaskToApiFuture<>(setValue(value));
    +    return setValueInternal(value, PriorityUtilities.parsePriority(this.path, null), null);
       }
     
       /**
    @@ -215,29 +213,6 @@ public ApiFuture setValueAsync(Object value) {
        * @return The ApiFuture for this operation.
        */
       public ApiFuture setValueAsync(Object value, Object priority) {
    -    return new TaskToApiFuture<>(setValue(value, priority));
    -  }
    -
    -  /**
    -   * Similar to {@link #setValueAsync(Object)} but returns a Task.
    -   *
    -   * @param value The value to set at this location
    -   * @return The {@link Task} for this operation.
    -   * @deprecated Use {@link #setValueAsync(Object)}
    -   */
    -  public Task setValue(Object value) {
    -    return setValueInternal(value, PriorityUtilities.parsePriority(this.path, null), null);
    -  }
    -
    -  /**
    -   * Similar to {@link #setValueAsync(Object, Object)} but returns a Task.
    -   *
    -   * @param value The value to set at this location
    -   * @param priority The priority to set at this location
    -   * @return The {@link Task} for this operation.
    -   * @deprecated Use {@link #setValueAsync(Object, Object)}
    -   */
    -  public Task setValue(Object value, Object priority) {
         return setValueInternal(value, PriorityUtilities.parsePriority(this.path, priority), null);
       }
     
    @@ -315,13 +290,14 @@ public void setValue(Object value, Object priority, CompletionListener listener)
         setValueInternal(value, PriorityUtilities.parsePriority(this.path, priority), listener);
       }
     
    -  private Task setValueInternal(Object value, Node priority, CompletionListener optListener) {
    +  private ApiFuture setValueInternal(Object value, Node priority, CompletionListener
    +      optListener) {
         Validation.validateWritablePath(getPath());
         ValidationPath.validateWithObject(getPath(), value);
         Object bouncedValue = CustomClassMapper.convertToPlainJavaTypes(value);
         Validation.validateWritableObject(bouncedValue);
         final Node node = NodeUtilities.NodeFromJSON(bouncedValue, priority);
    -    final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener);
    +    final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener);
         repo.scheduleNow(
             new Runnable() {
               @Override
    @@ -364,17 +340,6 @@ public void run() {
        * @return The ApiFuture for this operation.
        */
       public ApiFuture setPriorityAsync(Object priority) {
    -    return new TaskToApiFuture<>(setPriority(priority));
    -  }
    -
    -  /**
    -   * Similar to {@link #setPriorityAsync(Object)} but returns a Task.
    -   *
    -   * @param priority The priority to set at the specified location.
    -   * @return The {@link Task} for this operation.
    -   * @deprecated Use {@link #setPriorityAsync(Object)}
    -   */
    -  public Task setPriority(Object priority) {
         return setPriorityInternal(PriorityUtilities.parsePriority(this.path, priority), null);
       }
     
    @@ -413,10 +378,10 @@ public void setPriority(Object priority, CompletionListener listener) {
     
       // Remove
     
    -  private Task setPriorityInternal(final Node priority, CompletionListener optListener) {
    +  private ApiFuture setPriorityInternal(final Node priority, CompletionListener optListener) {
         Validation.validateWritablePath(getPath());
     
    -    final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener);
    +    final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener);
         repo.scheduleNow(
             new Runnable() {
               @Override
    @@ -438,17 +403,6 @@ public void run() {
        * @return The ApiFuture for this operation.
        */
       public ApiFuture updateChildrenAsync(Map update) {
    -    return new TaskToApiFuture<>(updateChildren(update));
    -  }
    -
    -  /**
    -   * Similar to {@link #updateChildrenAsync(Map)} but returns a Task.
    -   *
    -   * @param update The paths to update and their new values
    -   * @return The {@link Task} for this operation.
    -   * @deprecated Use {@link #updateChildrenAsync(Map)}
    -   */
    -  public Task updateChildren(Map update) {
         return updateChildrenInternal(update, null);
       }
     
    @@ -467,7 +421,7 @@ public void updateChildren(final Map update, final CompletionLis
     
       // Transactions
     
    -  private Task updateChildrenInternal(
    +  private ApiFuture updateChildrenInternal(
           final Map update, final CompletionListener optListener) {
         if (update == null) {
           throw new NullPointerException("Can't pass null for argument 'update' in updateChildren()");
    @@ -477,7 +431,7 @@ private Task updateChildrenInternal(
             Validation.parseAndValidateUpdate(getPath(), bouncedUpdate);
         final CompoundWrite merge = CompoundWrite.fromPathMerge(parsedUpdate);
     
    -    final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener);
    +    final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener);
         repo.scheduleNow(
             new Runnable() {
               @Override
    @@ -494,17 +448,7 @@ public void run() {
        * @return The ApiFuture for this operation.
        */
       public ApiFuture removeValueAsync() {
    -    return new TaskToApiFuture<>(removeValue());
    -  }
    -
    -  /**
    -   * Similar to {@link #removeValueAsync()} but returns a Task.
    -   *
    -   * @return The Task for this operation.
    -   * @deprecated Use {@link #removeValueAsync()}
    -   */
    -  public Task removeValue() {
    -    return setValue(null);
    +    return setValueAsync(null);
       }
     
       // Manual Connection Management
    diff --git a/src/main/java/com/google/firebase/database/FirebaseDatabase.java b/src/main/java/com/google/firebase/database/FirebaseDatabase.java
    index 6a196f913..2ddb55d58 100644
    --- a/src/main/java/com/google/firebase/database/FirebaseDatabase.java
    +++ b/src/main/java/com/google/firebase/database/FirebaseDatabase.java
    @@ -256,24 +256,6 @@ public void goOffline() {
         RepoManager.interrupt(ensureRepo());
       }
     
    -  /**
    -   * By default, this is set to {@link Logger.Level#INFO INFO}. This includes any internal errors
    -   * ({@link Logger.Level#ERROR ERROR}) and any security debug messages ({@link Logger.Level#INFO
    -   * INFO}) that the client receives. Set to {@link Logger.Level#DEBUG DEBUG} to turn on the
    -   * diagnostic logging, and {@link Logger.Level#NONE NONE} to disable all logging.
    -   *
    -   * @param logLevel The desired minimum log level
    -   * @deprecated This method will be removed in a future release. Use SLF4J-based logging instead.
    -   *     For example, add the slf4j-simple.jar to the classpath to log to STDERR. See
    -   *     SLF4J user manual for more details.
    -   */
    -  public synchronized void setLogLevel(Logger.Level logLevel) {
    -    synchronized (lock) {
    -      assertUnfrozen("setLogLevel");
    -      this.config.setLogLevel(logLevel);
    -    }
    -  }
    -
       /**
        * The Firebase Database client will cache synchronized data and keep track of all writes you've
        * initiated while your application is running. It seamlessly handles intermittent network
    diff --git a/src/main/java/com/google/firebase/database/MutableData.java b/src/main/java/com/google/firebase/database/MutableData.java
    index 79e87f2b4..7a0e7983b 100644
    --- a/src/main/java/com/google/firebase/database/MutableData.java
    +++ b/src/main/java/com/google/firebase/database/MutableData.java
    @@ -177,7 +177,7 @@ public String getKey() {
        *   
  • List<Object> * * - *

    This list is recursive; the possible types for {@link java.lang.Object} in the above list is + *

    This list is recursive; the possible types for Object in the above list is * given by the same list. These types correspond to the types available in JSON. * * @return The data contained in this instance as native types, or null if there is no data at @@ -190,7 +190,7 @@ public Object getValue() { /** * Due to the way that Java implements generics, it takes an extra step to get back a - * properly-typed Collection. So, in the case where you want a {@link java.util.List} of Message + * properly-typed Collection. So, in the case where you want a List of Message * instances, you will need to do something like the following: * *

    
    diff --git a/src/main/java/com/google/firebase/database/OnDisconnect.java b/src/main/java/com/google/firebase/database/OnDisconnect.java
    index 3b02987db..7b4cc0859 100644
    --- a/src/main/java/com/google/firebase/database/OnDisconnect.java
    +++ b/src/main/java/com/google/firebase/database/OnDisconnect.java
    @@ -28,8 +28,6 @@
     import com.google.firebase.database.utilities.Utilities;
     import com.google.firebase.database.utilities.Validation;
     import com.google.firebase.database.utilities.encoding.CustomClassMapper;
    -import com.google.firebase.internal.TaskToApiFuture;
    -import com.google.firebase.tasks.Task;
     
     import java.util.Map;
     
    @@ -52,41 +50,6 @@ public class OnDisconnect {
         this.path = path;
       }
     
    -  /**
    -   * Similar to {@link #setValueAsync(Object)}, but returns a Task.
    -   *
    -   * @param value The value to be set when a disconnect occurs
    -   * @return The {@link Task} for this operation.
    -   * @deprecated Use {@link #setValueAsync(Object)}
    -   */
    -  public Task setValue(Object value) {
    -    return onDisconnectSetInternal(value, PriorityUtilities.NullPriority(), null);
    -  }
    -
    -  /**
    -   * Similar to {@link #setValueAsync(Object, String)}, but returns a Task.
    -   *
    -   * @param value The value to be set when a disconnect occurs
    -   * @param priority The priority to be set when a disconnect occurs
    -   * @return The {@link Task} for this operation.
    -   * @deprecated Use {@link #setValueAsync(Object, String)}
    -   */
    -  public Task setValue(Object value, String priority) {
    -    return onDisconnectSetInternal(value, PriorityUtilities.parsePriority(path, priority), null);
    -  }
    -
    -  /**
    -   * Similar to {@link #setValueAsync(Object, double)}, but returns a Task.
    -   *
    -   * @param value The value to be set when a disconnect occurs
    -   * @param priority The priority to be set when a disconnect occurs
    -   * @return The {@link Task} for this operation.
    -   * @deprecated Use {@link #setValueAsync(Object, double)}
    -   */
    -  public Task setValue(Object value, double priority) {
    -    return onDisconnectSetInternal(value, PriorityUtilities.parsePriority(path, priority), null);
    -  }
    -
       /**
        * Ensure the data at this location is set to the specified value when the client is disconnected
        * (due to closing the browser, navigating to a new page, or network issues). 
    @@ -157,7 +120,7 @@ public void setValue(Object value, Map priority, CompletionListener listener) { * @return The ApiFuture for this operation. */ public ApiFuture setValueAsync(Object value) { - return new TaskToApiFuture<>(setValue(value)); + return onDisconnectSetInternal(value, PriorityUtilities.NullPriority(), null); } /** @@ -172,7 +135,7 @@ public ApiFuture setValueAsync(Object value) { * @return The ApiFuture for this operation. */ public ApiFuture setValueAsync(Object value, String priority) { - return new TaskToApiFuture<>(setValue(value, priority)); + return onDisconnectSetInternal(value, PriorityUtilities.parsePriority(path, priority), null); } /** @@ -187,17 +150,17 @@ public ApiFuture setValueAsync(Object value, String priority) { * @return The ApiFuture for this operation. */ public ApiFuture setValueAsync(Object value, double priority) { - return new TaskToApiFuture<>(setValue(value, priority)); + return onDisconnectSetInternal(value, PriorityUtilities.parsePriority(path, priority), null); } - private Task onDisconnectSetInternal( + private ApiFuture onDisconnectSetInternal( Object value, Node priority, final CompletionListener optListener) { Validation.validateWritablePath(path); ValidationPath.validateWithObject(path, value); Object bouncedValue = CustomClassMapper.convertToPlainJavaTypes(value); Validation.validateWritableObject(bouncedValue); final Node node = NodeUtilities.NodeFromJSON(bouncedValue, priority); - final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener); + final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener); repo.scheduleNow( new Runnable() { @Override @@ -210,17 +173,6 @@ public void run() { // Update - /** - * Similar to {@link #updateChildrenAsync(Map)}, but returns a Task. - * - * @param update The paths to update, along with their desired values - * @return The {@link Task} for this operation. - * @deprecated Use {@link #updateChildrenAsync(Map)} - */ - public Task updateChildren(Map update) { - return updateChildrenInternal(update, null); - } - /** * Ensure the data has the specified child values updated when the client is disconnected * @@ -238,13 +190,13 @@ public void updateChildren(final Map update, final CompletionLis * @return The ApiFuture for this operation. */ public ApiFuture updateChildrenAsync(Map update) { - return new TaskToApiFuture<>(updateChildren(update)); + return updateChildrenInternal(update, null); } - private Task updateChildrenInternal( + private ApiFuture updateChildrenInternal( final Map update, final CompletionListener optListener) { final Map parsedUpdate = Validation.parseAndValidateUpdate(path, update); - final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener); + final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener); repo.scheduleNow( new Runnable() { @Override @@ -257,16 +209,6 @@ public void run() { // Remove - /** - * Similar to {@link #removeValueAsync()}, but returns a Task. - * - * @return The {@link Task} for this operation. - * @deprecated Use {@link #removeValueAsync()} - */ - public Task removeValue() { - return setValue(null); - } - /** * Remove the value at this location when the client disconnects * @@ -282,21 +224,11 @@ public void removeValue(CompletionListener listener) { * @return The ApiFuture for this operation. */ public ApiFuture removeValueAsync() { - return new TaskToApiFuture<>(removeValue()); + return setValueAsync(null); } // Cancel the operation - /** - * Similar to {@link #cancelAsync()} ()}, but returns a Task. - * - * @return The {@link Task} for this operation. - * @deprecated Use {@link #cancelAsync()}. - */ - public Task cancel() { - return cancelInternal(null); - } - /** * Cancel any disconnect operations that are queued up at this location * @@ -312,11 +244,11 @@ public void cancel(final CompletionListener listener) { * @return The ApiFuture for this operation. */ public ApiFuture cancelAsync() { - return new TaskToApiFuture<>(cancel()); + return cancelInternal(null); } - private Task cancelInternal(final CompletionListener optListener) { - final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener); + private ApiFuture cancelInternal(final CompletionListener optListener) { + final Pair, CompletionListener> wrapped = Utilities.wrapOnComplete(optListener); repo.scheduleNow( new Runnable() { @Override diff --git a/src/main/java/com/google/firebase/database/connection/Connection.java b/src/main/java/com/google/firebase/database/connection/Connection.java index 4059e46e8..83e377304 100644 --- a/src/main/java/com/google/firebase/database/connection/Connection.java +++ b/src/main/java/com/google/firebase/database/connection/Connection.java @@ -17,10 +17,11 @@ package com.google.firebase.database.connection; import com.google.common.annotations.VisibleForTesting; -import com.google.firebase.database.logging.LogWrapper; import java.util.HashMap; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class Connection implements WebsocketConnection.Delegate { @@ -39,11 +40,14 @@ class Connection implements WebsocketConnection.Delegate { private static final String SERVER_HELLO_TIMESTAMP = "ts"; private static final String SERVER_HELLO_HOST = "h"; private static final String SERVER_HELLO_SESSION_ID = "s"; + + private static final Logger logger = LoggerFactory.getLogger(Connection.class); + private static long connectionIds = 0; - private final LogWrapper logger; private final HostInfo hostInfo; private final Delegate delegate; + private final String label; private WebsocketConnection conn; private State state; @@ -54,36 +58,31 @@ class Connection implements WebsocketConnection.Delegate { String cachedHost, Delegate delegate, String optLastSessionId) { - this(context, hostInfo, delegate, + this(hostInfo, delegate, new DefaultWebsocketConnectionFactory(context, hostInfo, cachedHost, optLastSessionId)); } @VisibleForTesting Connection( - ConnectionContext context, HostInfo hostInfo, Delegate delegate, WebsocketConnectionFactory connFactory) { long connId = connectionIds++; this.hostInfo = hostInfo; this.delegate = delegate; - this.logger = new LogWrapper(context.getLogger(), Connection.class, "conn_" + connId); + this.label = "[conn_" + connId + "]"; this.state = State.REALTIME_CONNECTING; this.conn = connFactory.newConnection(this); } public void open() { - if (logger.logsDebug()) { - logger.debug("Opening a connection"); - } + logger.debug("{} Opening a connection", label); conn.open(); } public void close(DisconnectReason reason) { if (state != State.REALTIME_DISCONNECTED) { - if (logger.logsDebug()) { - logger.debug("closing realtime connection"); - } + logger.debug("{} Closing realtime connection", label); state = State.REALTIME_DISCONNECTED; if (conn != null) { @@ -123,21 +122,14 @@ public void onMessage(Map message) { Map data = (Map) message.get(SERVER_ENVELOPE_DATA); onControlMessage(data); } else { - if (logger.logsDebug()) { - logger.debug("Ignoring unknown server message type: " + messageType); - } + logger.debug("{} Ignoring unknown server message type: {}", label, messageType); } } else { - if (logger.logsDebug()) { - logger.debug( - "Failed to parse server message: missing message type:" + message.toString()); - } + logger.debug("{} Failed to parse server message: missing message type: {}", label, message); close(); } } catch (ClassCastException e) { - if (logger.logsDebug()) { - logger.debug("Failed to parse server message: " + e.toString()); - } + logger.debug("{} Failed to parse server message", label, e); close(); } } @@ -146,30 +138,22 @@ public void onMessage(Map message) { public void onDisconnect(boolean wasEverConnected) { conn = null; if (!wasEverConnected && state == State.REALTIME_CONNECTING) { - if (logger.logsDebug()) { - logger.debug("Realtime connection failed"); - } + logger.debug("{} Realtime connection failed", label); } else { - if (logger.logsDebug()) { - logger.debug("Realtime connection lost"); - } + logger.debug("{} Realtime connection lost", label); } close(); } private void onDataMessage(Map data) { - if (logger.logsDebug()) { - logger.debug("received data message: " + data.toString()); - } + logger.debug("{} Received data message: {}", label, data); // We don't do anything with data messages, just kick them up a level delegate.onDataMessage(data); } private void onControlMessage(Map data) { - if (logger.logsDebug()) { - logger.debug("Got control message: " + data.toString()); - } + logger.debug("{} Got control message: {}", label, data); try { String messageType = (String) data.get(SERVER_CONTROL_MESSAGE_TYPE); if (messageType != null) { @@ -185,28 +169,20 @@ private void onControlMessage(Map data) { (Map) data.get(SERVER_CONTROL_MESSAGE_DATA); onHandshake(handshakeData); } else { - if (logger.logsDebug()) { - logger.debug("Ignoring unknown control message: " + messageType); - } + logger.debug("{} Ignoring unknown control message: {}", label, messageType); } } else { - if (logger.logsDebug()) { - logger.debug("Got invalid control message: " + data.toString()); - } + logger.debug("{} Got invalid control message: {}", label, data); close(); } } catch (ClassCastException e) { - if (logger.logsDebug()) { - logger.debug("Failed to parse control message: " + e.toString()); - } + logger.debug("{} Failed to parse control message", label, e); close(); } } private void onConnectionShutdown(String reason) { - if (logger.logsDebug()) { - logger.debug("Connection shutdown command received. Shutting down..."); - } + logger.debug("{} Connection shutdown command received. Shutting down...", label); delegate.onKill(reason); close(); } @@ -224,21 +200,15 @@ private void onHandshake(Map handshake) { } private void onConnectionReady(long timestamp, String sessionId) { - if (logger.logsDebug()) { - logger.debug("realtime connection established"); - } + logger.debug("{} Realtime connection established", label); state = State.REALTIME_CONNECTED; delegate.onReady(timestamp, sessionId); } private void onReset(String host) { - if (logger.logsDebug()) { - logger.debug( - "Got a reset; killing connection to " - + this.hostInfo.getHost() - + "; Updating internalHost to " - + host); - } + logger.debug( + "{} Got a reset; killing connection to {}; Updating internalHost to {}", + label, hostInfo.getHost(), host); delegate.onCacheHost(host); // Explicitly close the connection with SERVER_RESET so calling code knows to reconnect @@ -248,12 +218,12 @@ private void onReset(String host) { private void sendData(Map data, boolean isSensitive) { if (state != State.REALTIME_CONNECTED) { - logger.debug("Tried to send on an unconnected connection"); + logger.debug("{} Tried to send on an unconnected connection", label); } else { if (isSensitive) { - logger.debug("Sending data (contents hidden)"); + logger.debug("{} Sending data (contents hidden)", label); } else { - logger.debug("Sending data: %s", data); + logger.debug("{} Sending data: {}", label, data); } conn.send(data); } diff --git a/src/main/java/com/google/firebase/database/connection/ConnectionContext.java b/src/main/java/com/google/firebase/database/connection/ConnectionContext.java index 03b4b45f4..574bc1df9 100644 --- a/src/main/java/com/google/firebase/database/connection/ConnectionContext.java +++ b/src/main/java/com/google/firebase/database/connection/ConnectionContext.java @@ -16,8 +16,6 @@ package com.google.firebase.database.connection; -import com.google.firebase.database.logging.Logger; - import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; @@ -25,21 +23,18 @@ public class ConnectionContext { private final ScheduledExecutorService executorService; private final ConnectionAuthTokenProvider authTokenProvider; - private final Logger logger; private final boolean persistenceEnabled; private final String clientSdkVersion; private final String userAgent; private final ThreadFactory threadFactory; public ConnectionContext( - Logger logger, ConnectionAuthTokenProvider authTokenProvider, ScheduledExecutorService executorService, boolean persistenceEnabled, String clientSdkVersion, String userAgent, ThreadFactory threadFactory) { - this.logger = logger; this.authTokenProvider = authTokenProvider; this.executorService = executorService; this.persistenceEnabled = persistenceEnabled; @@ -48,10 +43,6 @@ public ConnectionContext( this.threadFactory = threadFactory; } - public Logger getLogger() { - return this.logger; - } - public ConnectionAuthTokenProvider getAuthTokenProvider() { return this.authTokenProvider; } diff --git a/src/main/java/com/google/firebase/database/connection/NettyWebSocketClient.java b/src/main/java/com/google/firebase/database/connection/NettyWebSocketClient.java index 40d172722..9769b65a8 100644 --- a/src/main/java/com/google/firebase/database/connection/NettyWebSocketClient.java +++ b/src/main/java/com/google/firebase/database/connection/NettyWebSocketClient.java @@ -5,8 +5,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.base.Strings; -import com.google.firebase.internal.GaeThreadFactory; -import com.google.firebase.internal.RevivingScheduledExecutor; +import com.google.firebase.internal.FirebaseScheduledExecutor; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; @@ -67,8 +66,8 @@ class NettyWebSocketClient implements WebsocketConnection.WSClient { this.uri = checkNotNull(uri, "uri must not be null"); this.eventHandler = checkNotNull(eventHandler, "event handler must not be null"); this.channelHandler = new WebSocketClientHandler(uri, userAgent, eventHandler); - this.executorService = new RevivingScheduledExecutor( - threadFactory, "firebase-websocket-worker", GaeThreadFactory.isAvailable()); + this.executorService = new FirebaseScheduledExecutor(threadFactory, + "firebase-websocket-worker"); this.group = new NioEventLoopGroup(1, this.executorService); } @@ -104,7 +103,7 @@ protected void initChannel(SocketChannel ch) { channelFuture.addListener( new ChannelFutureListener() { @Override - public void operationComplete(ChannelFuture future) throws Exception { + public void operationComplete(ChannelFuture future) { if (!future.isSuccess()) { eventHandler.onError(future.cause()); } @@ -174,7 +173,7 @@ public void channelInactive(ChannelHandlerContext context) { } @Override - public void channelRead0(ChannelHandlerContext context, Object message) throws Exception { + public void channelRead0(ChannelHandlerContext context, Object message) { Channel channel = context.channel(); if (message instanceof FullHttpResponse) { checkState(!handshaker.isHandshakeComplete()); diff --git a/src/main/java/com/google/firebase/database/connection/PersistentConnectionImpl.java b/src/main/java/com/google/firebase/database/connection/PersistentConnectionImpl.java index 5f2f71303..fc79b4cfe 100644 --- a/src/main/java/com/google/firebase/database/connection/PersistentConnectionImpl.java +++ b/src/main/java/com/google/firebase/database/connection/PersistentConnectionImpl.java @@ -19,7 +19,6 @@ import static com.google.firebase.database.connection.ConnectionUtils.hardAssert; import com.google.firebase.database.connection.util.RetryHelper; -import com.google.firebase.database.logging.LogWrapper; import com.google.firebase.database.util.GAuthToken; import java.util.ArrayList; @@ -33,6 +32,8 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class PersistentConnectionImpl implements Connection.Delegate, PersistentConnection { @@ -91,6 +92,9 @@ public class PersistentConnectionImpl implements Connection.Delegate, Persistent private static final String SERVER_KILL_INTERRUPT_REASON = "server_kill"; private static final String IDLE_INTERRUPT_REASON = "connection_idle"; private static final String TOKEN_REFRESH_INTERRUPT_REASON = "token_refresh"; + + private static final Logger logger = LoggerFactory.getLogger(PersistentConnection.class); + private static long connectionIds = 0; private final Delegate delegate; @@ -99,8 +103,8 @@ public class PersistentConnectionImpl implements Connection.Delegate, Persistent private final ConnectionFactory connFactory; private final ConnectionAuthTokenProvider authTokenProvider; private final ScheduledExecutorService executorService; - private final LogWrapper logger; private final RetryHelper retryHelper; + private final String label; private String cachedHost; private HashSet interruptReasons = new HashSet<>(); @@ -143,7 +147,7 @@ public PersistentConnectionImpl(ConnectionContext context, HostInfo info, Delega this.outstandingPuts = new HashMap<>(); this.onDisconnectRequestQueue = new ArrayList<>(); this.retryHelper = - new RetryHelper.Builder(this.executorService, context.getLogger(), RetryHelper.class) + new RetryHelper.Builder(this.executorService, RetryHelper.class) .withMinDelayAfterFailure(1000) .withRetryExponent(1.3) .withMaxDelay(30 * 1000) @@ -151,7 +155,7 @@ public PersistentConnectionImpl(ConnectionContext context, HostInfo info, Delega .build(); long connId = connectionIds++; - this.logger = new LogWrapper(context.getLogger(), PersistentConnection.class, "pc_" + connId); + this.label = "[pc_" + connId + "]"; this.lastSessionId = null; doIdleCheck(); } @@ -159,9 +163,7 @@ public PersistentConnectionImpl(ConnectionContext context, HostInfo info, Delega // Connection.Delegate methods @Override public void onReady(long timestamp, String sessionId) { - if (logger.logsDebug()) { - logger.debug("onReady"); - } + logger.debug("{} onReady", label); lastConnectionEstablishedTime = System.currentTimeMillis(); handleTimestamp(timestamp); @@ -188,16 +190,12 @@ public void listen( Long tag, RequestResultCallback listener) { ListenQuerySpec query = new ListenQuerySpec(path, queryParams); - if (logger.logsDebug()) { - logger.debug("Listening on " + query); - } + logger.debug("{} Listening on {}", label, query); // TODO: Fix this somehow? //hardAssert(query.isDefault() || !query.loadsAllData(), "listen() called for non-default but " // + "complete query"); hardAssert(!listens.containsKey(query), "listen() called twice for same QuerySpec."); - if (logger.logsDebug()) { - logger.debug("Adding listen query: " + query); - } + logger.debug("{} Adding listen query: {}", label, query); OutstandingListen outstandingListen = new OutstandingListen(listener, query, tag, currentHashFn); listens.put(query, outstandingListen); @@ -277,23 +275,19 @@ public void onDataMessage(Map message) { Map body = (Map) message.get(SERVER_ASYNC_PAYLOAD); onDataPush(action, body); } else { - if (logger.logsDebug()) { - logger.debug("Ignoring unknown message: " + message); - } + logger.debug("{} Ignoring unknown message: {}", label, message); } } @Override public void onDisconnect(Connection.DisconnectReason reason) { - if (logger.logsDebug()) { - logger.debug("Got on disconnect due to " + reason.name()); - } + logger.debug("{} Got on disconnect due to {}", label, reason.name()); this.connectionState = ConnectionState.Disconnected; this.realtime = null; this.hasOnDisconnects = false; requestCBHash.clear(); if (inactivityTimer != null) { - logger.debug("cancelling idle time checker"); + logger.debug("{} Cancelling idle time checker", label); inactivityTimer.cancel(false); inactivityTimer = null; } @@ -319,21 +313,16 @@ public void onDisconnect(Connection.DisconnectReason reason) { @Override public void onKill(String reason) { - if (logger.logsDebug()) { - logger.debug( - "Firebase Database connection was forcefully killed by the server. Will not attempt " - + "reconnect. Reason: " - + reason); - } + logger.debug( + "{} Firebase Database connection was forcefully killed by the server. Will not attempt " + + "reconnect. Reason: {}", label, reason); interrupt(SERVER_KILL_INTERRUPT_REASON); } @Override public void unlisten(List path, Map queryParams) { ListenQuerySpec query = new ListenQuerySpec(path, queryParams); - if (logger.logsDebug()) { - logger.debug("unlistening on " + query); - } + logger.debug("{} Unlistening on {}", label, query); // TODO: fix this by understanding query params? //Utilities.hardAssert(query.isDefault() || !query.loadsAllData(), @@ -395,9 +384,7 @@ public void onDisconnectCancel(List path, RequestResultCallback onComple @Override public void interrupt(String reason) { - if (logger.logsDebug()) { - logger.debug("Connection interrupted for: " + reason); - } + logger.debug("{} Connection interrupted for: {}", label, reason); interruptReasons.add(reason); if (realtime != null) { @@ -414,10 +401,7 @@ public void interrupt(String reason) { @Override public void resume(String reason) { - if (logger.logsDebug()) { - logger.debug("Connection no longer interrupted for: " + reason); - } - + logger.debug("{} Connection no longer interrupted for: {}", label, reason); interruptReasons.remove(reason); if (shouldReconnect() && connectionState == ConnectionState.Disconnected) { @@ -444,7 +428,7 @@ public void refreshAuthToken() { // we close the connection to make sure any writes/listens are queued until the connection // is reauthed with the current token after reconnecting. Note that this will trigger // onDisconnects which isn't ideal. - logger.debug("Auth token refresh requested"); + logger.debug("{} Auth token refresh requested", label); // By using interrupt instead of closing the connection we make sure there are no race // conditions with other fetch token attempts (interrupt/resume is expected to handle those @@ -455,7 +439,7 @@ public void refreshAuthToken() { @Override public void refreshAuthToken(String token) { - logger.debug("Auth token refreshed."); + logger.debug("{} Auth token refreshed.", label); this.authToken = token; if (connected()) { if (token != null) { @@ -473,13 +457,13 @@ private void tryScheduleReconnect() { "Not in disconnected state: %s", this.connectionState); final boolean forceRefresh = this.forceAuthTokenRefresh; - logger.debug("Scheduling connection attempt"); + logger.debug("{} Scheduling connection attempt", label); this.forceAuthTokenRefresh = false; retryHelper.retry( new Runnable() { @Override public void run() { - logger.debug("Trying to fetch auth token"); + logger.debug("{} Trying to fetch auth token", label); hardAssert( connectionState == ConnectionState.Disconnected, "Not in disconnected state: %s", @@ -496,7 +480,7 @@ public void onSuccess(String token) { // Someone could have interrupted us while fetching the token, // marking the connection as Disconnected if (connectionState == ConnectionState.GettingToken) { - logger.debug("Successfully fetched token, opening connection"); + logger.debug("{} Successfully fetched token, opening connection", label); openNetworkConnection(token); } else { hardAssert( @@ -504,13 +488,13 @@ public void onSuccess(String token) { "Expected connection state disconnected, but was %s", connectionState); logger.debug( - "Not opening connection after token refresh, " - + "because connection was set to disconnected"); + "{} Not opening connection after token refresh, because connection " + + "was set to disconnected", label); } } else { logger.debug( - "Ignoring getToken result, because this was not the " - + "latest attempt."); + "{} Ignoring getToken result, because this was not the " + + "latest attempt.", label); } } @@ -518,12 +502,12 @@ public void onSuccess(String token) { public void onError(String error) { if (thisGetTokenAttempt == currentGetTokenAttempt) { connectionState = ConnectionState.Disconnected; - logger.debug("Error fetching token: " + error); + logger.debug("{} Error fetching token: {}", label, error); tryScheduleReconnect(); } else { logger.debug( - "Ignoring getToken error, because this was not the " - + "latest attempt."); + "{} Ignoring getToken error, because this was not the " + + "latest attempt.", label); } } }); @@ -609,14 +593,10 @@ private void sendUnlisten(OutstandingListen listen) { } private OutstandingListen removeListen(ListenQuerySpec query) { - if (logger.logsDebug()) { - logger.debug("removing query " + query); - } + logger.debug("{} removing query {}", label, query); if (!listens.containsKey(query)) { - if (logger.logsDebug()) { - logger.debug( - "Trying to remove listener for QuerySpec " + query + " but no listener exists."); - } + logger.debug( + "{} Trying to remove listener for QuerySpec {} but no listener exists.", label, query); return null; } else { OutstandingListen oldListen = listens.get(query); @@ -627,9 +607,7 @@ private OutstandingListen removeListen(ListenQuerySpec query) { } private Collection removeListens(List path) { - if (logger.logsDebug()) { - logger.debug("removing all listens at path " + path); - } + logger.debug("{} Removing all listens at path {}", label, path); List removedListens = new ArrayList<>(); for (Map.Entry entry : listens.entrySet()) { ListenQuerySpec query = entry.getKey(); @@ -649,9 +627,7 @@ private Collection removeListens(List path) { } private void onDataPush(String action, Map body) { - if (logger.logsDebug()) { - logger.debug("handleServerMessage: " + action + " " + body); - } + logger.debug("{} handleServerMessage: {} {}", label, action, body); if (action.equals(SERVER_ASYNC_DATA_UPDATE) || action.equals(SERVER_ASYNC_DATA_MERGE)) { boolean isMerge = action.equals(SERVER_ASYNC_DATA_MERGE); @@ -660,9 +636,7 @@ private void onDataPush(String action, Map body) { Long tagNumber = ConnectionUtils.longFromObject(body.get(SERVER_DATA_TAG)); // ignore empty merges if (isMerge && (payloadData instanceof Map) && ((Map) payloadData).size() == 0) { - if (logger.logsDebug()) { - logger.debug("ignoring empty merge for path " + pathString); - } + logger.debug("{} Ignoring empty merge for path {}", label, pathString); } else { List path = ConnectionUtils.stringToPath(pathString); delegate.onDataUpdate(path, payloadData, isMerge, tagNumber); @@ -684,9 +658,7 @@ private void onDataPush(String action, Map body) { rangeMerges.add(new RangeMerge(start, end, update)); } if (rangeMerges.isEmpty()) { - if (logger.logsDebug()) { - logger.debug("Ignoring empty range merge for path " + pathString); - } + logger.debug("{} Ignoring empty range merge for path {}", label, pathString); } else { this.delegate.onRangeMergeUpdate(path, rangeMerges, tag); } @@ -701,9 +673,7 @@ private void onDataPush(String action, Map body) { } else if (action.equals(SERVER_ASYNC_SECURITY_DEBUG)) { onSecurityDebugPacket(body); } else { - if (logger.logsDebug()) { - logger.debug("Unrecognized action from server: " + action); - } + logger.debug("{} Unrecognized action from server: {}", label, action); } } @@ -723,7 +693,7 @@ private void onAuthRevoked(String errorCode, String errorMessage) { // This might be for an earlier token than we just recently sent. But since we need to close // the connection anyways, we can set it to null here and we will refresh the token later // on reconnect. - logger.debug("Auth token revoked: " + errorCode + " (" + errorMessage + ")"); + logger.debug("{} Auth token revoked: {} ({})", label, errorCode, errorMessage); this.authToken = null; this.forceAuthTokenRefresh = true; this.delegate.onAuthStatus(false); @@ -733,7 +703,7 @@ private void onAuthRevoked(String errorCode, String errorMessage) { private void onSecurityDebugPacket(Map message) { // TODO: implement on iOS too - logger.info((String) message.get("msg")); + logger.info("{} {}", label, message.get("msg")); } private void upgradeAuth() { @@ -766,7 +736,7 @@ public void onResponse(Map response) { forceAuthTokenRefresh = true; delegate.onAuthStatus(false); String reason = (String) response.get(SERVER_RESPONSE_DATA); - logger.debug("Authentication failed: " + status + " (" + reason + ")"); + logger.debug("{} Authentication failed: {} ({})", label, status, reason); realtime.close(); if (status.equals("invalid_token") || status.equals("permission_denied")) { @@ -778,11 +748,11 @@ public void onResponse(Map response) { // Set a long reconnect delay because recovery is unlikely. retryHelper.setMaxDelay(); logger.warn( - "Provided authentication credentials are invalid. This " + "{} Provided authentication credentials are invalid. This " + "usually indicates your FirebaseApp instance was not initialized " + "correctly. Make sure your database URL is correct and that your " + "service account is for the correct project and is authorized to " - + "access it."); + + "access it.", label); } } } @@ -814,9 +784,7 @@ private void sendUnauth() { } private void restoreAuth() { - if (logger.logsDebug()) { - logger.debug("calling restore state"); - } + logger.debug("{} Calling restore state", label); hardAssert( this.connectionState == ConnectionState.Connecting, @@ -824,15 +792,11 @@ private void restoreAuth() { this.connectionState); if (authToken == null) { - if (logger.logsDebug()) { - logger.debug("Not restoring auth because token is null."); - } + logger.debug("{} Not restoring auth because token is null.", label); this.connectionState = ConnectionState.Connected; restoreState(); } else { - if (logger.logsDebug()) { - logger.debug("Restoring auth."); - } + logger.debug("{} Restoring auth.", label); this.connectionState = ConnectionState.Authenticating; sendAuthAndRestoreState(); } @@ -845,19 +809,13 @@ private void restoreState() { this.connectionState); // Restore listens - if (logger.logsDebug()) { - logger.debug("Restoring outstanding listens"); - } + logger.debug("{} Restoring outstanding listens", label); for (OutstandingListen listen : listens.values()) { - if (logger.logsDebug()) { - logger.debug("Restoring listen " + listen.getQuery()); - } + logger.debug("{} Restoring listen {}", label, listen.getQuery()); sendListen(listen); } - if (logger.logsDebug()) { - logger.debug("Restoring writes."); - } + logger.debug("{} Restoring writes.", label); // Restore puts ArrayList outstanding = new ArrayList<>(outstandingPuts.keySet()); // Make sure puts are restored in order @@ -878,9 +836,7 @@ private void restoreState() { } private void handleTimestamp(long timestamp) { - if (logger.logsDebug()) { - logger.debug("handling timestamp"); - } + logger.debug("{} Handling timestamp", label); long timestampDelta = timestamp - System.currentTimeMillis(); Map updates = new HashMap<>(); updates.put(Constants.DOT_INFO_SERVERTIME_OFFSET, timestampDelta); @@ -930,9 +886,7 @@ assert canSendWrites() new ConnectionRequestCallback() { @Override public void onResponse(Map response) { - if (logger.logsDebug()) { - logger.debug(action + " response: " + response); - } + logger.debug("{} {} response: {}", label, action, response); OutstandingPut currentPut = outstandingPuts.get(putId); if (currentPut == put) { @@ -948,10 +902,8 @@ public void onResponse(Map response) { } } } else { - if (logger.logsDebug()) { - logger.debug( - "Ignoring on complete for put " + putId + " because it was removed already."); - } + logger.debug("{} Ignoring on complete for put {} because it was removed already.", + label, putId); } doIdleCheck(); } @@ -1032,17 +984,13 @@ public void onResponse(Map response) { String status = (String) response.get(REQUEST_STATUS); if (!status.equals("ok")) { String errorMessage = (String) response.get(SERVER_DATA_UPDATE_BODY); - if (logger.logsDebug()) { - logger.debug( - "Failed to send stats: " + status + " (message: " + errorMessage + ")"); - } + logger.debug( + "{} Failed to send stats: {} (message: {})", label, stats, errorMessage); } } }); } else { - if (logger.logsDebug()) { - logger.debug("Not sending stats because stats are empty"); - } + logger.debug("{} Not sending stats because stats are empty", label); } } @@ -1051,11 +999,9 @@ private void warnOnListenerWarnings(List warnings, ListenQuerySpec query if (warnings.contains("no_index")) { String indexSpec = "\".indexOn\": \"" + query.queryParams.get("i") + '\"'; logger.warn( - "Using an unspecified index. Consider adding '" - + indexSpec - + "' at " - + ConnectionUtils.pathToString(query.path) - + " to your security and Firebase Database rules for better performance"); + "{} Using an unspecified index. Consider adding '{}' at {} to your security and " + + "Firebase Database rules for better performance", + label, indexSpec, ConnectionUtils.pathToString(query.path)); } } @@ -1064,9 +1010,7 @@ private void sendConnectStats() { assert !this.context.isPersistenceEnabled() : "Stats for persistence on JVM missing (persistence not yet supported)"; stats.put("sdk.admin_java." + context.getClientSdkVersion().replace('.', '-'), 1); - if (logger.logsDebug()) { - logger.debug("Sending first connection stats"); - } + logger.debug("{} Sending first connection stats", label); sendStats(stats); } diff --git a/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java b/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java index 9b28a19dd..3e29db07a 100644 --- a/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java +++ b/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java @@ -20,7 +20,6 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; -import com.google.firebase.database.logging.LogWrapper; import com.google.firebase.database.util.JsonMapper; import java.io.EOFException; @@ -33,6 +32,8 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Represents a WebSocket connection to the Firebase Realtime Database. This abstraction acts as @@ -47,11 +48,12 @@ class WebsocketConnection { private static final long CONNECT_TIMEOUT_MS = 30 * 1000; // 30 seconds private static final int MAX_FRAME_SIZE = 16384; private static final AtomicLong CONN_ID = new AtomicLong(0); + private static final Logger logger = LoggerFactory.getLogger(WebsocketConnection.class); private final ScheduledExecutorService executorService; - private final LogWrapper logger; private final WSClient conn; private final Delegate delegate; + private final String label; private StringList buffer; private boolean everConnected = false; @@ -75,8 +77,7 @@ class WebsocketConnection { WSClientFactory clientFactory) { this.executorService = connectionContext.getExecutorService(); this.delegate = delegate; - this.logger = new LogWrapper(connectionContext.getLogger(), WebsocketConnection.class, - "ws_" + CONN_ID.getAndIncrement()); + this.label = "[ws_" + CONN_ID.getAndIncrement() + "]"; this.conn = clientFactory.newClient(new WSClientHandlerImpl()); } @@ -99,9 +100,7 @@ void start() { } void close() { - if (logger.logsDebug()) { - logger.debug("websocket is being closed"); - } + logger.debug("{} Websocket is being closed", label); isClosed = true; conn.close(); @@ -130,7 +129,7 @@ void send(Map message) { conn.send(seg); } } catch (IOException e) { - logger.error("Failed to serialize message: " + message.toString(), e); + logger.error("{} Failed to serialize message: {}", label, message, e); closeAndNotify(); } } @@ -150,9 +149,7 @@ private List splitIntoFrames(String src, int maxFrameSize) { } private void handleNewFrameCount(int numFrames) { - if (logger.logsDebug()) { - logger.debug("HandleNewFrameCount: " + numFrames); - } + logger.debug("{} Handle new frame count: {}", label, numFrames); buffer = new StringList(numFrames); } @@ -165,15 +162,13 @@ private void appendFrame(String message) { String combined = buffer.combine(); try { Map decoded = JsonMapper.parseJson(combined); - if (logger.logsDebug()) { - logger.debug("handleIncomingFrame complete frame: " + decoded); - } + logger.debug("{} Parsed complete frame: {}", label, decoded); delegate.onMessage(decoded); } catch (IOException e) { - logger.error("Error parsing frame: " + combined, e); + logger.error("{} Error parsing frame: {}", label, combined, e); closeAndNotify(); } catch (ClassCastException e) { - logger.error("Error parsing frame (cast error): " + combined, e); + logger.error("{} Error parsing frame (cast error): {}", label, combined, e); closeAndNotify(); } } @@ -218,13 +213,10 @@ private void resetKeepAlive() { } if (keepAlive != null) { keepAlive.cancel(false); - if (logger.logsDebug()) { - logger.debug("Reset keepAlive. Remaining: " + keepAlive.getDelay(TimeUnit.MILLISECONDS)); - } + logger.debug("{} Reset keepAlive. Remaining: {}", label, + keepAlive.getDelay(TimeUnit.MILLISECONDS)); } else { - if (logger.logsDebug()) { - logger.debug("Reset keepAlive"); - } + logger.debug("{} Reset keepAlive", label); } keepAlive = executorService.schedule(nop(), KEEP_ALIVE_TIMEOUT_MS, TimeUnit.MILLISECONDS); } @@ -253,18 +245,14 @@ private void closeAndNotify() { private void onClosed() { if (!isClosed) { - if (logger.logsDebug()) { - logger.debug("closing itself"); - } + logger.debug("{} Closing itself", label); closeAndNotify(); } } private void closeIfNeverConnected() { if (!everConnected && !isClosed) { - if (logger.logsDebug()) { - logger.debug("timed out on connect"); - } + logger.debug("{} Timed out on connect", label); closeAndNotify(); } } @@ -278,9 +266,7 @@ private class WSClientHandlerImpl implements WSClientEventHandler { @Override public void onOpen() { - if (logger.logsDebug()) { - logger.debug("websocket opened"); - } + logger.debug("{} Websocket opened", label); executorService.execute(new Runnable() { @Override public void run() { @@ -293,9 +279,7 @@ public void run() { @Override public void onMessage(final String message) { - if (logger.logsDebug()) { - logger.debug("ws message: " + message); - } + logger.debug("{} WS message: {}", label, message); executorService.execute(new Runnable() { @Override public void run() { @@ -306,9 +290,7 @@ public void run() { @Override public void onClose() { - if (logger.logsDebug()) { - logger.debug("closed"); - } + logger.debug("{} Closed", label); if (!isClosed) { // If the connection tear down was initiated by the higher-layer, isClosed will already // be true. Nothing more to do in that case. @@ -325,9 +307,9 @@ public void run() { @Override public void onError(final Throwable e) { if (e instanceof EOFException || e.getCause() instanceof EOFException) { - logger.debug("WebSocket reached EOF", e); + logger.debug("{} WebSocket reached EOF", label, e); } else { - logger.error("WebSocket error", e); + logger.error("{} WebSocket error", label, e); } executorService.execute( new Runnable() { diff --git a/src/main/java/com/google/firebase/database/connection/util/RetryHelper.java b/src/main/java/com/google/firebase/database/connection/util/RetryHelper.java index 2563a30ca..f8f6bb77a 100644 --- a/src/main/java/com/google/firebase/database/connection/util/RetryHelper.java +++ b/src/main/java/com/google/firebase/database/connection/util/RetryHelper.java @@ -16,18 +16,18 @@ package com.google.firebase.database.connection.util; -import com.google.firebase.database.logging.LogWrapper; -import com.google.firebase.database.logging.Logger; - import java.util.Random; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class RetryHelper { + private static final Logger logger = LoggerFactory.getLogger(RetryHelper.class); + private final ScheduledExecutorService executorService; - private final LogWrapper logger; /** The minimum delay for a retry in ms. */ private final long minRetryDelayAfterFailure; /** The maximum retry delay in ms. */ @@ -49,13 +49,11 @@ public class RetryHelper { private RetryHelper( ScheduledExecutorService executorService, - LogWrapper logger, long minRetryDelayAfterFailure, long maxRetryDelay, double retryExponent, double jitterFactor) { this.executorService = executorService; - this.logger = logger; this.minRetryDelayAfterFailure = minRetryDelayAfterFailure; this.maxRetryDelay = maxRetryDelay; this.retryExponent = retryExponent; @@ -84,7 +82,7 @@ public void retry(final Runnable runnable) { + (jitterFactor * currentRetryDelay * random.nextDouble())); } this.lastWasSuccess = false; - logger.debug("Scheduling retry in %dms", delay); + logger.debug("Scheduling retry in {}ms", delay); Runnable wrapped = new Runnable() { @Override @@ -119,15 +117,13 @@ public void cancel() { public static class Builder { private final ScheduledExecutorService service; - private final LogWrapper logger; private long minRetryDelayAfterFailure = 1000; private double jitterFactor = 0.5; private long retryMaxDelay = 30 * 1000; private double retryExponent = 1.3; - public Builder(ScheduledExecutorService service, Logger logger, Class tag) { + public Builder(ScheduledExecutorService service, Class tag) { this.service = service; - this.logger = new LogWrapper(logger, tag); } public Builder withMinDelayAfterFailure(long delay) { @@ -156,7 +152,6 @@ public Builder withJitterFactor(double random) { public RetryHelper build() { return new RetryHelper( this.service, - this.logger, this.minRetryDelayAfterFailure, this.retryMaxDelay, this.retryExponent, diff --git a/src/main/java/com/google/firebase/database/core/Context.java b/src/main/java/com/google/firebase/database/core/Context.java index 52c07b8c6..da68861cc 100644 --- a/src/main/java/com/google/firebase/database/core/Context.java +++ b/src/main/java/com/google/firebase/database/core/Context.java @@ -26,29 +26,24 @@ import com.google.firebase.database.connection.PersistentConnection; import com.google.firebase.database.core.persistence.NoopPersistenceManager; import com.google.firebase.database.core.persistence.PersistenceManager; -import com.google.firebase.database.logging.LogWrapper; -import com.google.firebase.database.logging.Logger; import com.google.firebase.database.utilities.DefaultRunLoop; -import java.util.List; import java.util.concurrent.ScheduledExecutorService; public class Context { private static final long DEFAULT_CACHE_SIZE = 10 * 1024 * 1024; - protected Logger logger; - protected EventTarget eventTarget; - protected AuthTokenProvider authTokenProvider; - protected RunLoop runLoop; - protected String persistenceKey; - protected List loggedComponents; - protected String userAgent; - protected Logger.Level logLevel = Logger.Level.INFO; - protected boolean persistenceEnabled; - protected long cacheSize = DEFAULT_CACHE_SIZE; - protected FirebaseApp firebaseApp; - private PersistenceManager forcedPersistenceManager; + FirebaseApp firebaseApp; + + EventTarget eventTarget; + AuthTokenProvider authTokenProvider; + RunLoop runLoop; + String persistenceKey; + boolean persistenceEnabled; + long cacheSize = DEFAULT_CACHE_SIZE; + + private String userAgent; private boolean frozen = false; private boolean stopped = false; @@ -78,19 +73,11 @@ public void onError(String error) { private Platform getPlatform() { if (platform == null) { - if (GaePlatform.isActive()) { - platform = new GaePlatform(firebaseApp); - } else { - platform = new JvmPlatform(firebaseApp); - } + platform = new JvmPlatform(firebaseApp); } return platform; } - public boolean isFrozen() { - return frozen; - } - public boolean isStopped() { return stopped; } @@ -110,8 +97,6 @@ public void requireStarted() { } private void initServices() { - // Do the logger first, so that other components can get a LogWrapper - ensureLogger(); // Cache platform getPlatform(); ensureUserAgent(); @@ -137,28 +122,15 @@ void stop() { } } - protected void assertUnfrozen() { - if (isFrozen()) { + void assertUnfrozen() { + if (frozen) { throw new DatabaseException( "Modifications to DatabaseConfig objects must occur before they are in use"); } } - public LogWrapper getLogger(String component) { - return new LogWrapper(logger, component, null); - } - - public LogWrapper getLogger(Class component) { - return new LogWrapper(logger, component); - } - - public LogWrapper getLogger(Class component, String prefix) { - return new LogWrapper(logger, component, prefix); - } - public ConnectionContext getConnectionContext() { return new ConnectionContext( - this.logger, wrapAuthTokenProvider(this.getAuthTokenProvider()), this.getExecutorService(), this.isPersistenceEnabled(), @@ -168,10 +140,6 @@ public ConnectionContext getConnectionContext() { } PersistenceManager getPersistenceManager(String firebaseId) { - // TODO[persistence]: Create this once and store it. - if (forcedPersistenceManager != null) { - return forcedPersistenceManager; - } if (this.persistenceEnabled) { PersistenceManager cache = platform.createPersistenceManager(this, firebaseId); if (cache == null) { @@ -193,11 +161,6 @@ public long getPersistenceCacheSizeBytes() { return this.cacheSize; } - // For testing - void forcePersistenceManager(PersistenceManager persistenceManager) { - this.forcedPersistenceManager = persistenceManager; - } - public EventTarget getEventTarget() { return eventTarget; } @@ -210,14 +173,6 @@ public String getUserAgent() { return userAgent; } - public String getPlatformVersion() { - return getPlatform().getPlatformVersion(); - } - - public String getSessionPersistenceKey() { - return this.persistenceKey; - } - public AuthTokenProvider getAuthTokenProvider() { return this.authTokenProvider; } @@ -237,12 +192,6 @@ private ScheduledExecutorService getExecutorService() { return ((DefaultRunLoop) loop).getExecutorService(); } - private void ensureLogger() { - if (logger == null) { - logger = getPlatform().newLogger(this, logLevel, loggedComponents); - } - } - private void ensureRunLoop() { if (runLoop == null) { runLoop = platform.newRunLoop(this); diff --git a/src/main/java/com/google/firebase/database/core/DatabaseConfig.java b/src/main/java/com/google/firebase/database/core/DatabaseConfig.java index 491098d6a..844f59266 100644 --- a/src/main/java/com/google/firebase/database/core/DatabaseConfig.java +++ b/src/main/java/com/google/firebase/database/core/DatabaseConfig.java @@ -18,9 +18,6 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.database.DatabaseException; -import com.google.firebase.database.Logger; - -import java.util.List; /** * TODO: Since this is no longer public, we should merge it with Context and clean all @@ -29,21 +26,6 @@ */ public class DatabaseConfig extends Context { - // TODO: Remove this from the public API since we currently can't pass logging - // across AIDL interface. - - /** - * If you would like to provide a custom log target, pass an object that implements the {@link - * com.google.firebase.database.Logger Logger} interface. - * - * @hide - * @param logger The custom logger that will be called with all log messages - */ - public synchronized void setLogger(com.google.firebase.database.logging.Logger logger) { - assertUnfrozen(); - this.logger = logger; - } - /** * In the default setup, the Firebase Database library will create a thread to handle all * callbacks. On Android, it will attempt to use the main debugComponents) { - assertUnfrozen(); - setLogLevel(Logger.Level.DEBUG); - loggedComponents = debugComponents; - } - public void setRunLoop(RunLoop runLoop) { this.runLoop = runLoop; } diff --git a/src/main/java/com/google/firebase/database/core/GaePlatform.java b/src/main/java/com/google/firebase/database/core/GaePlatform.java deleted file mode 100644 index b7698f3de..000000000 --- a/src/main/java/com/google/firebase/database/core/GaePlatform.java +++ /dev/null @@ -1,131 +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.core; - -import com.google.firebase.FirebaseApp; -import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.database.FirebaseDatabase; -import com.google.firebase.database.connection.ConnectionContext; -import com.google.firebase.database.connection.HostInfo; -import com.google.firebase.database.connection.PersistentConnection; -import com.google.firebase.database.connection.PersistentConnectionImpl; -import com.google.firebase.database.core.persistence.PersistenceManager; -import com.google.firebase.database.logging.DefaultLogger; -import com.google.firebase.database.logging.LogWrapper; -import com.google.firebase.database.logging.Logger; -import com.google.firebase.database.utilities.DefaultRunLoop; -import com.google.firebase.internal.GaeThreadFactory; -import com.google.firebase.internal.RevivingScheduledExecutor; - -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; - -/** - * Represents a Google AppEngine platform. - * - *

    This class is not thread-safe. - */ -class GaePlatform implements Platform { - - private static final String PROCESS_PLATFORM = "AppEngine"; - private final FirebaseApp firebaseApp; - - public GaePlatform(FirebaseApp firebaseApp) { - this.firebaseApp = firebaseApp; - } - - public static boolean isActive() { - return GaeThreadFactory.isAvailable(); - } - - @Override - public Logger newLogger(Context ctx, Logger.Level level, List components) { - return new DefaultLogger(level, components); - } - - private ThreadFactory getGaeThreadFactory() { - return ImplFirebaseTrampolines.getThreadFactory(firebaseApp); - } - - @Override - public EventTarget newEventTarget(Context ctx) { - RevivingScheduledExecutor eventExecutor = - new RevivingScheduledExecutor(getGaeThreadFactory(), "FirebaseDatabaseEventTarget", true); - return new ThreadPoolEventTarget(eventExecutor); - } - - @Override - public RunLoop newRunLoop(final Context context) { - final LogWrapper logger = context.getLogger(RunLoop.class); - return new DefaultRunLoop(getGaeThreadFactory(), /* periodicRestart= */ true, context) { - @Override - public void handleException(Throwable e) { - logger.error(DefaultRunLoop.messageForException(e), e); - } - }; - } - - @Override - public AuthTokenProvider newAuthTokenProvider(ScheduledExecutorService executorService) { - return new JvmAuthTokenProvider(this.firebaseApp, executorService); - } - - @Override - public PersistentConnection newPersistentConnection( - Context context, - ConnectionContext connectionContext, - HostInfo info, - PersistentConnection.Delegate delegate) { - return new PersistentConnectionImpl(context.getConnectionContext(), info, delegate); - } - - @Override - public String getUserAgent(Context ctx) { - return PROCESS_PLATFORM + "/" + DEVICE; - } - - @Override - public String getPlatformVersion() { - return "gae-" + FirebaseDatabase.getSdkVersion(); - } - - @Override - public PersistenceManager createPersistenceManager(Context ctx, String namespace) { - return null; - } - - @Override - public ThreadInitializer getThreadInitializer() { - return new ThreadInitializer() { - @Override - public void setName(Thread t, String name) { - // Unsupported by GAE - } - - @Override - public void setDaemon(Thread t, boolean isDaemon) { - // Unsupported by GAE - } - - @Override - public void setUncaughtExceptionHandler(Thread t, Thread.UncaughtExceptionHandler handler) { - // Unsupported by GAE - } - }; - } -} diff --git a/src/main/java/com/google/firebase/database/core/JvmPlatform.java b/src/main/java/com/google/firebase/database/core/JvmPlatform.java index 30a2e25df..2a226c078 100644 --- a/src/main/java/com/google/firebase/database/core/JvmPlatform.java +++ b/src/main/java/com/google/firebase/database/core/JvmPlatform.java @@ -24,14 +24,12 @@ import com.google.firebase.database.connection.PersistentConnection; import com.google.firebase.database.connection.PersistentConnectionImpl; import com.google.firebase.database.core.persistence.PersistenceManager; -import com.google.firebase.database.logging.DefaultLogger; -import com.google.firebase.database.logging.LogWrapper; -import com.google.firebase.database.logging.Logger; import com.google.firebase.database.utilities.DefaultRunLoop; -import java.util.List; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class JvmPlatform implements Platform { @@ -39,24 +37,19 @@ class JvmPlatform implements Platform { private final FirebaseApp firebaseApp; - public JvmPlatform(FirebaseApp firebaseApp) { + JvmPlatform(FirebaseApp firebaseApp) { this.firebaseApp = firebaseApp; } - @Override - public Logger newLogger(Context ctx, Logger.Level level, List components) { - return new DefaultLogger(level, components); - } - @Override public EventTarget newEventTarget(Context ctx) { ThreadFactory threadFactory = ImplFirebaseTrampolines.getThreadFactory(firebaseApp); - return new ThreadPoolEventTarget(threadFactory, ThreadInitializer.defaultInstance); + return new ThreadPoolEventTarget(threadFactory); } @Override public RunLoop newRunLoop(final Context context) { - final LogWrapper logger = context.getLogger(RunLoop.class); + final Logger logger = LoggerFactory.getLogger(RunLoop.class); ThreadFactory threadFactory = ImplFirebaseTrampolines.getThreadFactory(firebaseApp); return new DefaultRunLoop(threadFactory) { @Override @@ -94,9 +87,4 @@ public String getPlatformVersion() { public PersistenceManager createPersistenceManager(Context ctx, String namespace) { return null; } - - @Override - public ThreadInitializer getThreadInitializer() { - return ThreadInitializer.defaultInstance; - } } diff --git a/src/main/java/com/google/firebase/database/core/Platform.java b/src/main/java/com/google/firebase/database/core/Platform.java index 5e701c408..2d2742a62 100644 --- a/src/main/java/com/google/firebase/database/core/Platform.java +++ b/src/main/java/com/google/firebase/database/core/Platform.java @@ -20,17 +20,13 @@ import com.google.firebase.database.connection.HostInfo; import com.google.firebase.database.connection.PersistentConnection; import com.google.firebase.database.core.persistence.PersistenceManager; -import com.google.firebase.database.logging.Logger; -import java.util.List; import java.util.concurrent.ScheduledExecutorService; public interface Platform { String DEVICE = "AdminJava"; - Logger newLogger(Context ctx, Logger.Level level, List components); - EventTarget newEventTarget(Context ctx); RunLoop newRunLoop(Context ctx); @@ -48,6 +44,4 @@ PersistentConnection newPersistentConnection( String getPlatformVersion(); PersistenceManager createPersistenceManager(Context ctx, String firebaseId); - - ThreadInitializer getThreadInitializer(); } diff --git a/src/main/java/com/google/firebase/database/core/Repo.java b/src/main/java/com/google/firebase/database/core/Repo.java index add6ff3e0..98777d04d 100644 --- a/src/main/java/com/google/firebase/database/core/Repo.java +++ b/src/main/java/com/google/firebase/database/core/Repo.java @@ -38,7 +38,6 @@ import com.google.firebase.database.core.view.Event; import com.google.firebase.database.core.view.EventRaiser; import com.google.firebase.database.core.view.QuerySpec; -import com.google.firebase.database.logging.LogWrapper; import com.google.firebase.database.snapshot.ChildKey; import com.google.firebase.database.snapshot.EmptyNode; import com.google.firebase.database.snapshot.IndexedNode; @@ -54,6 +53,8 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Repo implements PersistentConnection.Delegate { @@ -67,14 +68,14 @@ public class Repo implements PersistentConnection.Delegate { private static final String TRANSACTION_TOO_MANY_RETRIES = "maxretries"; private static final String TRANSACTION_OVERRIDE_BY_SET = "overriddenBySet"; + + private static final Logger logger = LoggerFactory.getLogger(Repo.class); + private final RepoInfo repoInfo; private final OffsetClock serverClock = new OffsetClock(new DefaultClock(), 0); private final PersistentConnection connection; private final EventRaiser eventRaiser; private final Context ctx; - private final LogWrapper operationLogger; - private final LogWrapper transactionLogger; - private final LogWrapper dataLogger; private SnapshotHolder infoData; private SparseSnapshotTree onDisconnect; private Tree> transactionQueueTree; @@ -90,11 +91,6 @@ public class Repo implements PersistentConnection.Delegate { this.repoInfo = repoInfo; this.ctx = ctx; this.database = database; - - operationLogger = this.ctx.getLogger(Repo.class); - transactionLogger = this.ctx.getLogger(Repo.class.getName() + ".Transaction"); - dataLogger = this.ctx.getLogger(Repo.class.getName() + ".DataOperation"); - this.eventRaiser = new EventRaiser(this.ctx); HostInfo hostInfo = new HostInfo(repoInfo.host, repoInfo.namespace, repoInfo.secure); @@ -134,7 +130,7 @@ private void deferredInitialization() { new AuthTokenProvider.TokenChangeListener() { @Override public void onTokenChange(String token) { - operationLogger.debug("Auth token changed, triggering auth token refresh"); + logger.debug("Auth token changed, triggering auth token refresh"); connection.refreshAuthToken(token); } }); @@ -150,7 +146,7 @@ public void onTokenChange(String token) { transactionQueueTree = new Tree<>(); - infoSyncTree = new SyncTree(ctx, new NoopPersistenceManager(), + infoSyncTree = new SyncTree(new NoopPersistenceManager(), new SyncTree.ListenProvider() { @Override public void startListening( @@ -178,7 +174,7 @@ public void run() { public void stopListening(QuerySpec query, Tag tag) {} }); - serverSyncTree = new SyncTree(ctx, persistenceManager, + serverSyncTree = new SyncTree(persistenceManager, new SyncTree.ListenProvider() { @Override public void startListening( @@ -263,12 +259,8 @@ boolean hasListeners() { public void onDataUpdate( List pathSegments, Object message, boolean isMerge, Long optTag) { Path path = new Path(pathSegments); - if (operationLogger.logsDebug()) { - operationLogger.debug("onDataUpdate: " + path); - } - if (dataLogger.logsDebug()) { - operationLogger.debug("onDataUpdate: " + path + " " + message); - } + logger.debug("onDataUpdate: {} {}", path, message); + List events; try { @@ -306,7 +298,7 @@ public void onDataUpdate( postEvents(events); } catch (DatabaseException e) { - operationLogger.error("FIREBASE INTERNAL ERROR", e); + logger.error("Firebase internal error", e); } } @@ -316,12 +308,7 @@ public void onRangeMergeUpdate( List merges, Long tagNumber) { Path path = new Path(pathSegments); - if (operationLogger.logsDebug()) { - operationLogger.debug("onRangeMergeUpdate: " + path); - } - if (dataLogger.logsDebug()) { - operationLogger.debug("onRangeMergeUpdate: " + path + " " + merges); - } + logger.debug("onRangeMergeUpdate: {} {}", path, merges); List parsedMerges = new ArrayList<>(merges.size()); for (com.google.firebase.database.connection.RangeMerge merge : merges) { @@ -383,12 +370,7 @@ public void setValue( final Path path, Node newValueUnresolved, final DatabaseReference.CompletionListener onComplete) { - if (operationLogger.logsDebug()) { - operationLogger.debug("set: " + path); - } - if (dataLogger.logsDebug()) { - dataLogger.debug("set: " + path + " " + newValueUnresolved); - } + logger.debug("set: {} {}", path, newValueUnresolved); Map serverValues = ServerValues.generateServerValues(serverClock); Node newValue = ServerValues.resolveDeferredValueSnapshot(newValueUnresolved, serverValues); @@ -421,16 +403,9 @@ public void updateChildren( CompoundWrite updates, final DatabaseReference.CompletionListener onComplete, Map unParsedUpdates) { - if (operationLogger.logsDebug()) { - operationLogger.debug("update: " + path); - } - if (dataLogger.logsDebug()) { - dataLogger.debug("update: " + path + " " + unParsedUpdates); - } + logger.debug("update: {} {}", path, unParsedUpdates); if (updates.isEmpty()) { - if (operationLogger.logsDebug()) { - operationLogger.debug("update called with no changes. No-op"); - } + logger.debug("update called with no changes. No-op"); // dispatch on complete callOnComplete(onComplete, null, path); return; @@ -468,9 +443,7 @@ public void onRequestResult(String optErrorCode, String optErrorMessage) { } public void purgeOutstandingWrites() { - if (operationLogger.logsDebug()) { - operationLogger.debug("Purging writes"); - } + logger.debug("Purging writes"); List events = serverSyncTree.removeAllWrites(); postEvents(events); // Abort any transactions @@ -619,7 +592,7 @@ private void updateInfo(ChildKey childKey, Object value) { List events = this.infoSyncTree.applyServerOverwrite(path, node); this.postEvents(events); } catch (DatabaseException e) { - operationLogger.error("Failed to parse info update", e); + logger.error("Failed to parse info update", e); } } @@ -652,21 +625,16 @@ private void warnIfWriteFailed(String writeType, Path path, DatabaseError error) if (error != null && !(error.getCode() == DatabaseError.DATA_STALE || error.getCode() == DatabaseError.WRITE_CANCELED)) { - operationLogger.warn(writeType + " at " + path.toString() + " failed: " + error.toString()); + logger.warn(writeType + " at " + path.toString() + " failed: " + error.toString()); } } public void startTransaction(Path path, final Transaction.Handler handler, boolean applyLocally) { - if (operationLogger.logsDebug()) { - operationLogger.debug("transaction: " + path); - } - if (dataLogger.logsDebug()) { - operationLogger.debug("transaction: " + path); - } + logger.debug("transaction: {}", path); if (this.ctx.isPersistenceEnabled() && !loggedTransactionPersistenceWarning) { loggedTransactionPersistenceWarning = true; - transactionLogger.info( + logger.info( "runTransaction() usage detected while persistence is enabled. Please be aware that " + "transactions *will not* be persisted across database restarts. See " + "https://www.firebase.com/docs/android/guide/offline-capabilities.html" @@ -1142,11 +1110,7 @@ public void visitTree(Tree> tree) { private Path abortTransactions(Path path, final int reason) { Path affectedPath = getAncestorTransactionNode(path).getPath(); - - if (transactionLogger.logsDebug()) { - operationLogger.debug( - "Aborting transactions for path: " + path + ". Affected: " + affectedPath); - } + logger.debug("Aborting transactions for path: {}. Affected: {}", path, affectedPath); Tree> transactionNode = transactionQueueTree.subTree(path); transactionNode.forEachAncestor( @@ -1247,7 +1211,7 @@ private void runTransactionOnComplete(Transaction.Handler handler, DatabaseError try { handler.onComplete(error, committed, snapshot); } catch (Exception e) { - operationLogger.error("Exception in transaction onComplete callback", e); + logger.error("Exception in transaction onComplete callback", e); } } diff --git a/src/main/java/com/google/firebase/database/core/SyncTree.java b/src/main/java/com/google/firebase/database/core/SyncTree.java index 17fcad445..f7da11750 100644 --- a/src/main/java/com/google/firebase/database/core/SyncTree.java +++ b/src/main/java/com/google/firebase/database/core/SyncTree.java @@ -37,7 +37,6 @@ import com.google.firebase.database.core.view.Event; import com.google.firebase.database.core.view.QuerySpec; import com.google.firebase.database.core.view.View; -import com.google.firebase.database.logging.LogWrapper; import com.google.firebase.database.snapshot.ChildKey; import com.google.firebase.database.snapshot.CompoundHash; import com.google.firebase.database.snapshot.EmptyNode; @@ -57,6 +56,8 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * SyncTree is the central class for managing event callback registration, data caching, views @@ -80,6 +81,9 @@ public class SyncTree { // Size after which we start including the compound hash private static final long SIZE_THRESHOLD_FOR_COMPOUND_HASH = 1024; + + private static final Logger logger = LoggerFactory.getLogger(SyncTree.class); + /** * A tree of all pending user writes (user-initiated set()'s, transaction()'s, update()'s, etc.). */ @@ -90,14 +94,13 @@ public class SyncTree { private final Set keepSyncedQueries; private final ListenProvider listenProvider; private final PersistenceManager persistenceManager; - private final LogWrapper logger; /** Tree of SyncPoints. There's a SyncPoint at any location that has 1 or more views. */ private ImmutableTree syncPointTree; /** Static tracker for next query tag. */ private long nextQueryTag = 1L; public SyncTree( - Context context, PersistenceManager persistenceManager, ListenProvider listenProvider) { + PersistenceManager persistenceManager, ListenProvider listenProvider) { this.syncPointTree = ImmutableTree.emptyInstance(); this.pendingWriteTree = new WriteTree(); this.tagToQueryMap = new HashMap<>(); @@ -105,7 +108,6 @@ public SyncTree( this.keepSyncedQueries = new HashSet<>(); this.listenProvider = listenProvider; this.persistenceManager = persistenceManager; - this.logger = context.getLogger(SyncTree.class); } public boolean isEmpty() { @@ -968,7 +970,7 @@ public List onListenComplete(DatabaseError error) { return SyncTree.this.applyListenComplete(query.getPath()); } } else { - logger.warn("Listen at " + view.getQuery().getPath() + " failed: " + error.toString()); + logger.warn("Listen at {} failed: {}", view.getQuery().getPath(), error); // If a listen failed, kill all of the listeners here, not just the one that triggered the // error. Note that this may need to be scoped to just this listener if we change diff --git a/src/main/java/com/google/firebase/database/core/ThreadInitializer.java b/src/main/java/com/google/firebase/database/core/ThreadInitializer.java deleted file mode 100644 index 23c3cab88..000000000 --- a/src/main/java/com/google/firebase/database/core/ThreadInitializer.java +++ /dev/null @@ -1,46 +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.core; - -import java.lang.Thread.UncaughtExceptionHandler; - -public interface ThreadInitializer { - - ThreadInitializer defaultInstance = - new ThreadInitializer() { - @Override - public void setName(Thread t, String name) { - t.setName(name); - } - - @Override - public void setDaemon(Thread t, boolean isDaemon) { - t.setDaemon(isDaemon); - } - - @Override - public void setUncaughtExceptionHandler(Thread t, UncaughtExceptionHandler handler) { - t.setUncaughtExceptionHandler(handler); - } - }; - - void setName(Thread t, String name); - - void setDaemon(Thread t, boolean isDaemon); - - void setUncaughtExceptionHandler(Thread t, Thread.UncaughtExceptionHandler handler); -} diff --git a/src/main/java/com/google/firebase/database/core/ThreadPoolEventTarget.java b/src/main/java/com/google/firebase/database/core/ThreadPoolEventTarget.java index abe599fdc..34162fbe2 100644 --- a/src/main/java/com/google/firebase/database/core/ThreadPoolEventTarget.java +++ b/src/main/java/com/google/firebase/database/core/ThreadPoolEventTarget.java @@ -16,18 +16,15 @@ package com.google.firebase.database.core; -import static com.google.common.base.Preconditions.checkNotNull; - +import com.google.firebase.internal.FirebaseScheduledExecutor; import java.lang.Thread.UncaughtExceptionHandler; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** ThreadPoolEventTarget is an event target using a configurable threadpool. */ +/** ThreadPoolEventTarget is an event target using a configurable thread pool. */ class ThreadPoolEventTarget implements EventTarget, UncaughtExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(ThreadPoolEventTarget.class); @@ -35,26 +32,9 @@ class ThreadPoolEventTarget implements EventTarget, UncaughtExceptionHandler { private final ThreadPoolExecutor executor; private UncaughtExceptionHandler exceptionHandler; - public ThreadPoolEventTarget( - final ThreadFactory wrappedFactory, final ThreadInitializer threadInitializer) { - int poolSize = 1; - BlockingQueue queue = new LinkedBlockingQueue<>(); - - executor = new ThreadPoolExecutor(poolSize, poolSize, 3, TimeUnit.SECONDS, queue, - new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - Thread thread = wrappedFactory.newThread(r); - threadInitializer.setName(thread, "FirebaseDatabaseEventTarget"); - threadInitializer.setDaemon(thread, true); - threadInitializer.setUncaughtExceptionHandler(thread, ThreadPoolEventTarget.this); - return thread; - } - }); - } - - public ThreadPoolEventTarget(final ThreadPoolExecutor executor) { - this.executor = checkNotNull(executor); + ThreadPoolEventTarget(ThreadFactory threadFactory) { + executor = new FirebaseScheduledExecutor(threadFactory, "firebase-database-event-target", this); + executor.setKeepAliveTime(3, TimeUnit.SECONDS); } @Override diff --git a/src/main/java/com/google/firebase/database/core/persistence/DefaultPersistenceManager.java b/src/main/java/com/google/firebase/database/core/persistence/DefaultPersistenceManager.java index 1bc546295..40e1b99dc 100644 --- a/src/main/java/com/google/firebase/database/core/persistence/DefaultPersistenceManager.java +++ b/src/main/java/com/google/firebase/database/core/persistence/DefaultPersistenceManager.java @@ -17,12 +17,10 @@ package com.google.firebase.database.core.persistence; import com.google.firebase.database.core.CompoundWrite; -import com.google.firebase.database.core.Context; import com.google.firebase.database.core.Path; import com.google.firebase.database.core.UserWriteRecord; import com.google.firebase.database.core.view.CacheNode; import com.google.firebase.database.core.view.QuerySpec; -import com.google.firebase.database.logging.LogWrapper; import com.google.firebase.database.snapshot.ChildKey; import com.google.firebase.database.snapshot.EmptyNode; import com.google.firebase.database.snapshot.IndexedNode; @@ -34,24 +32,26 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class DefaultPersistenceManager implements PersistenceManager { + private static final Logger logger = LoggerFactory.getLogger(PersistenceManager.class); + private final PersistenceStorageEngine storageLayer; private final TrackedQueryManager trackedQueryManager; - private final LogWrapper logger; private final CachePolicy cachePolicy; private long serverCacheUpdatesSinceLastPruneCheck = 0; public DefaultPersistenceManager( - Context ctx, PersistenceStorageEngine engine, CachePolicy cachePolicy) { - this(ctx, engine, cachePolicy, new DefaultClock()); + PersistenceStorageEngine engine, CachePolicy cachePolicy) { + this(engine, cachePolicy, new DefaultClock()); } public DefaultPersistenceManager( - Context ctx, PersistenceStorageEngine engine, CachePolicy cachePolicy, Clock clock) { + PersistenceStorageEngine engine, CachePolicy cachePolicy, Clock clock) { this.storageLayer = engine; - this.logger = ctx.getLogger(PersistenceManager.class); this.trackedQueryManager = new TrackedQueryManager(storageLayer, logger, clock); this.cachePolicy = cachePolicy; } @@ -253,15 +253,11 @@ public T runInTransaction(Callable callable) { private void doPruneCheckAfterServerUpdate() { serverCacheUpdatesSinceLastPruneCheck++; if (cachePolicy.shouldCheckCacheSize(serverCacheUpdatesSinceLastPruneCheck)) { - if (logger.logsDebug()) { - logger.debug("Reached prune check threshold."); - } + logger.debug("Reached prune check threshold."); serverCacheUpdatesSinceLastPruneCheck = 0; boolean canPrune = true; long cacheSize = storageLayer.serverCacheEstimatedSizeInBytes(); - if (logger.logsDebug()) { - logger.debug("Cache size: " + cacheSize); - } + logger.debug("Cache size: {}", cacheSize); while (canPrune && cachePolicy.shouldPrune(cacheSize, trackedQueryManager.countOfPrunableQueries())) { PruneForest pruneForest = this.trackedQueryManager.pruneOldQueries(cachePolicy); @@ -271,9 +267,7 @@ private void doPruneCheckAfterServerUpdate() { canPrune = false; } cacheSize = storageLayer.serverCacheEstimatedSizeInBytes(); - if (logger.logsDebug()) { - logger.debug("Cache size after prune: " + cacheSize); - } + logger.debug("Cache size after prune: {}", cacheSize); } } } diff --git a/src/main/java/com/google/firebase/database/core/persistence/TrackedQueryManager.java b/src/main/java/com/google/firebase/database/core/persistence/TrackedQueryManager.java index b4f6b545c..20278b947 100644 --- a/src/main/java/com/google/firebase/database/core/persistence/TrackedQueryManager.java +++ b/src/main/java/com/google/firebase/database/core/persistence/TrackedQueryManager.java @@ -23,7 +23,6 @@ import com.google.firebase.database.core.utilities.Predicate; import com.google.firebase.database.core.view.QueryParams; import com.google.firebase.database.core.view.QuerySpec; -import com.google.firebase.database.logging.LogWrapper; import com.google.firebase.database.snapshot.ChildKey; import com.google.firebase.database.utilities.Clock; import com.google.firebase.database.utilities.Utilities; @@ -36,6 +35,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.slf4j.Logger; public class TrackedQueryManager { @@ -74,7 +74,7 @@ public boolean evaluate(TrackedQuery query) { }; // DB, where we permanently store tracked queries. private final PersistenceStorageEngine storageLayer; - private final LogWrapper logger; + private final Logger logger; private final Clock clock; // In-memory cache of tracked queries. Should always be in-sync with the DB. private ImmutableTree> trackedQueryTree; @@ -82,7 +82,7 @@ public boolean evaluate(TrackedQuery query) { private long currentQueryId = 0; public TrackedQueryManager( - PersistenceStorageEngine storageLayer, LogWrapper logger, Clock clock) { + PersistenceStorageEngine storageLayer, Logger logger, Clock clock) { this.storageLayer = storageLayer; this.logger = logger; this.clock = clock; @@ -226,13 +226,8 @@ public PruneForest pruneOldQueries(CachePolicy cachePolicy) { long countToPrune = calculateCountToPrune(cachePolicy, prunable.size()); PruneForest forest = new PruneForest(); - if (logger.logsDebug()) { - logger.debug( - "Pruning old queries. Prunable: " - + prunable.size() - + " Count to prune: " - + countToPrune); - } + logger.debug( + "Pruning old queries. Prunable: {} Count to prune: {}", prunable.size(), countToPrune); Collections.sort( prunable, @@ -257,9 +252,7 @@ public int compare(TrackedQuery q1, TrackedQuery q2) { // Also keep the unprunable queries. List unprunable = getQueriesMatching(IS_QUERY_UNPRUNABLE_PREDICATE); - if (logger.logsDebug()) { - logger.debug("Unprunable queries: " + unprunable.size()); - } + logger.debug("Unprunable queries: {}", unprunable.size()); for (TrackedQuery toKeep : unprunable) { forest = forest.keep(toKeep.querySpec.getPath()); } diff --git a/src/main/java/com/google/firebase/database/core/view/EventRaiser.java b/src/main/java/com/google/firebase/database/core/view/EventRaiser.java index 47579abd8..d2bbacee6 100644 --- a/src/main/java/com/google/firebase/database/core/view/EventRaiser.java +++ b/src/main/java/com/google/firebase/database/core/view/EventRaiser.java @@ -18,10 +18,11 @@ import com.google.firebase.database.core.Context; import com.google.firebase.database.core.EventTarget; -import com.google.firebase.database.logging.LogWrapper; import java.util.ArrayList; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Each view owns an instance of this class, and it is used to send events to the event target @@ -33,18 +34,16 @@ */ public class EventRaiser { + private static final Logger logger = LoggerFactory.getLogger(EventRaiser.class); + private final EventTarget eventTarget; - private final LogWrapper logger; public EventRaiser(Context ctx) { eventTarget = ctx.getEventTarget(); - logger = ctx.getLogger(EventRaiser.class); } public void raiseEvents(final List events) { - if (logger.logsDebug()) { - logger.debug("Raising " + events.size() + " event(s)"); - } + logger.debug("Raising {} event(s)", events.size()); // TODO: Use an immutable data structure for events so we don't have to clone to be safe. final ArrayList eventsClone = new ArrayList<>(events); eventTarget.postEvent( @@ -52,9 +51,7 @@ public void raiseEvents(final List events) { @Override public void run() { for (Event event : eventsClone) { - if (logger.logsDebug()) { - logger.debug("Raising " + event.toString()); - } + logger.debug("Raising {}", event); event.fire(); } } diff --git a/src/main/java/com/google/firebase/database/logging/DefaultLogger.java b/src/main/java/com/google/firebase/database/logging/DefaultLogger.java deleted file mode 100644 index d9ba110c7..000000000 --- a/src/main/java/com/google/firebase/database/logging/DefaultLogger.java +++ /dev/null @@ -1,93 +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.logging; - -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public class DefaultLogger implements Logger { - - private final Set enabledComponents; - private final Level minLevel; - - public DefaultLogger(Level level, List enabledComponents) { - if (enabledComponents != null) { - this.enabledComponents = new HashSet<>(enabledComponents); - } else { - this.enabledComponents = null; - } - minLevel = level; - } - - @Override - public Level getLogLevel() { - return this.minLevel; - } - - @Override - public void onLogMessage(Level level, String tag, String message, long msTimestamp) { - if (shouldLog(level, tag)) { - String toLog = buildLogMessage(level, tag, message, msTimestamp); - switch (level) { - case ERROR: - error(tag, toLog); - break; - case WARN: - warn(tag, toLog); - break; - case INFO: - info(tag, toLog); - break; - case DEBUG: - debug(tag, toLog); - break; - default: - throw new RuntimeException("Should not reach here!"); - } - } - } - - protected String buildLogMessage(Level level, String tag, String message, long msTimestamp) { - Date now = new Date(msTimestamp); - return now.toString() + " " + "[" + level + "] " + tag + ": " + message; - } - - protected void error(String tag, String toLog) { - System.err.println(toLog); - } - - protected void warn(String tag, String toLog) { - System.out.println(toLog); - } - - protected void info(String tag, String toLog) { - System.out.println(toLog); - } - - protected void debug(String tag, String toLog) { - System.out.println(toLog); - } - - protected boolean shouldLog(Level level, String tag) { - return (level.ordinal() >= minLevel.ordinal() - && (enabledComponents == null - || level.ordinal() > Level.DEBUG.ordinal() - || enabledComponents.contains(tag))); - } -} diff --git a/src/main/java/com/google/firebase/database/logging/LogWrapper.java b/src/main/java/com/google/firebase/database/logging/LogWrapper.java deleted file mode 100644 index 21e28e526..000000000 --- a/src/main/java/com/google/firebase/database/logging/LogWrapper.java +++ /dev/null @@ -1,127 +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.logging; - -import java.io.PrintWriter; -import java.io.StringWriter; -import org.slf4j.LoggerFactory; - -/** - * Legacy logging interface for database implementation. This class attempts to reconcile SLF4J - * with the old logging implementation of the Admin SDK. When SLF4J is available, logs using that - * API. Otherwise falls back to the old logging implementation. This prevents individual log - * statements from being written to both log APIs. - * - * @deprecated This class will be removed in a future release, and SLF4J will be used universally - * throughout the codebase. - */ -public class LogWrapper { - - private final org.slf4j.Logger slf4jLogger; - private final Logger logger; - private final String component; - private final String prefix; - - public LogWrapper(Logger logger, Class component) { - this(logger, component, null); - } - - public LogWrapper(Logger logger, Class component, String prefix) { - this.slf4jLogger = LoggerFactory.getLogger(component); - this.logger = logger; - this.component = component.getName(); - this.prefix = prefix; - } - - public LogWrapper(Logger logger, String component, String prefix) { - this.slf4jLogger = LoggerFactory.getLogger(component); - this.logger = logger; - this.component = component; - this.prefix = prefix; - } - - private static String exceptionStacktrace(Throwable e) { - StringWriter writer = new StringWriter(); - PrintWriter printWriter = new PrintWriter(writer); - e.printStackTrace(printWriter); - return writer.toString(); - } - - public void error(String message, Throwable e) { - if (slf4jLogger.isErrorEnabled()) { - slf4jLogger.error(toLog(message), e); - } else { - String logMsg = toLog(message) + "\n" + exceptionStacktrace(e); - logger.onLogMessage(Logger.Level.ERROR, component, logMsg, now()); - } - } - - public void warn(String message) { - warn(message, null); - } - - public void warn(String message, Throwable e) { - if (slf4jLogger.isWarnEnabled()) { - slf4jLogger.warn(toLog(message), e); - } else { - String logMsg = toLog(message); - if (e != null) { - logMsg = logMsg + "\n" + exceptionStacktrace(e); - } - logger.onLogMessage(Logger.Level.WARN, component, logMsg, now()); - } - } - - public void info(String message) { - if (slf4jLogger.isInfoEnabled()) { - slf4jLogger.info(toLog(message)); - } else { - logger.onLogMessage(Logger.Level.INFO, component, toLog(message), now()); - } - } - - public void debug(String message, Object... args) { - this.debug(message, null, args); - } - - /** Log a non-fatal exception. Typically something like an IO error on a failed connection */ - public void debug(String message, Throwable e, Object... args) { - if (slf4jLogger.isDebugEnabled()) { - slf4jLogger.debug(toLog(message, args), e); - } else { - String logMsg = toLog(message, args); - if (e != null) { - logMsg = logMsg + "\n" + exceptionStacktrace(e); - } - logger.onLogMessage(Logger.Level.DEBUG, component, logMsg, now()); - } - } - - public boolean logsDebug() { - return this.logger.getLogLevel().ordinal() <= Logger.Level.DEBUG.ordinal() - || slf4jLogger.isDebugEnabled(); - } - - private long now() { - return System.currentTimeMillis(); - } - - private String toLog(String message, Object... args) { - String formatted = (args.length > 0) ? String.format(message, args) : message; - return prefix == null ? formatted : prefix + " - " + formatted; - } -} diff --git a/src/main/java/com/google/firebase/database/logging/Logger.java b/src/main/java/com/google/firebase/database/logging/Logger.java deleted file mode 100644 index 7fd2e6e0e..000000000 --- a/src/main/java/com/google/firebase/database/logging/Logger.java +++ /dev/null @@ -1,47 +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.logging; - -/** - * Private (internal) logging interface used by Firebase Database. See {@link - * com.google.firebase.database.core.DatabaseConfig DatabaseConfig} for more information. - * - * @deprecated Use SLF4J-based logging - */ -public interface Logger { - - /** - * This method will be triggered whenever the library has something to log - * - * @param level The level of the log message - * @param tag The component that this log message is coming from - * @param message The message to be logged - * @param msTimestamp The timestamp, in milliseconds, at which this message was generated - */ - void onLogMessage(Level level, String tag, String message, long msTimestamp); - - Level getLogLevel(); - - /** The log levels used by the Firebase Database library */ - enum Level { - DEBUG, - INFO, - WARN, - ERROR, - NONE - } -} diff --git a/src/main/java/com/google/firebase/database/utilities/DefaultRunLoop.java b/src/main/java/com/google/firebase/database/utilities/DefaultRunLoop.java index d43deb6b3..e84b7b561 100644 --- a/src/main/java/com/google/firebase/database/utilities/DefaultRunLoop.java +++ b/src/main/java/com/google/firebase/database/utilities/DefaultRunLoop.java @@ -18,13 +18,13 @@ import com.google.firebase.database.DatabaseException; import com.google.firebase.database.FirebaseDatabase; -import com.google.firebase.database.annotations.Nullable; -import com.google.firebase.database.core.Context; -import com.google.firebase.database.core.RepoManager; import com.google.firebase.database.core.RunLoop; -import com.google.firebase.internal.RevivingScheduledExecutor; +import com.google.firebase.internal.FirebaseScheduledExecutor; import java.lang.Thread.UncaughtExceptionHandler; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -33,43 +33,39 @@ public abstract class DefaultRunLoop implements RunLoop { - private ScheduledThreadPoolExecutor executor; + private final ScheduledThreadPoolExecutor executor; private UncaughtExceptionHandler exceptionHandler; - /** Creates a DefaultRunLoop that does not periodically restart its threads. */ - public DefaultRunLoop(ThreadFactory threadFactory) { - this(threadFactory, false, null); - } - /** * Creates a DefaultRunLoop that optionally restarts its threads periodically. If 'context' is * provided, these restarts will automatically interrupt and resume all Repo connections. */ - public DefaultRunLoop( - final ThreadFactory threadFactory, - final boolean periodicRestart, - @Nullable final Context context) { - executor = - new RevivingScheduledExecutor(threadFactory, "FirebaseDatabaseWorker", periodicRestart) { - @Override - protected void handleException(Throwable throwable) { - DefaultRunLoop.this.handleExceptionInternal(throwable); - } - - @Override - protected void beforeRestart() { - if (context != null) { - RepoManager.interrupt(context); + protected DefaultRunLoop(ThreadFactory threadFactory) { + executor = new FirebaseScheduledExecutor(threadFactory, "firebase-database-worker") { + @Override + protected void afterExecute(Runnable runnable, Throwable throwable) { + super.afterExecute(runnable, throwable); + if (throwable == null && runnable instanceof Future) { + Future future = (Future) runnable; + try { + // Not all Futures will be done, e.g. when used with scheduledAtFixedRate + if (future.isDone()) { + future.get(); } + } catch (CancellationException ce) { + // Cancellation exceptions are okay, we expect them to happen sometimes + } catch (ExecutionException ee) { + throwable = ee.getCause(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } + } - @Override - protected void afterRestart() { - if (context != null) { - RepoManager.resume(context); - } - } - }; + if (throwable != null) { + handleExceptionInternal(throwable); + } + } + }; // Core threads don't time out, this only takes effect when we drop the number of required // core threads diff --git a/src/main/java/com/google/firebase/database/utilities/Utilities.java b/src/main/java/com/google/firebase/database/utilities/Utilities.java index bd055d273..88c932e49 100644 --- a/src/main/java/com/google/firebase/database/utilities/Utilities.java +++ b/src/main/java/com/google/firebase/database/utilities/Utilities.java @@ -16,14 +16,14 @@ package com.google.firebase.database.utilities; +import com.google.api.core.ApiFuture; +import com.google.api.core.SettableApiFuture; import com.google.common.io.BaseEncoding; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseException; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.core.Path; import com.google.firebase.database.core.RepoInfo; -import com.google.firebase.tasks.Task; -import com.google.firebase.tasks.TaskCompletionSource; import java.io.UnsupportedEncodingException; import java.net.URI; @@ -237,22 +237,22 @@ public static void hardAssert(boolean condition, String message) { } } - public static Pair, DatabaseReference.CompletionListener> wrapOnComplete( + public static Pair, DatabaseReference.CompletionListener> wrapOnComplete( DatabaseReference.CompletionListener optListener) { if (optListener == null) { - final TaskCompletionSource source = new TaskCompletionSource<>(); + final SettableApiFuture future = SettableApiFuture.create(); DatabaseReference.CompletionListener listener = new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError error, DatabaseReference ref) { if (error != null) { - source.setException(error.toException()); + future.setException(error.toException()); } else { - source.setResult(null); + future.set(null); } } }; - return new Pair<>(source.getTask(), listener); + return new Pair, DatabaseReference.CompletionListener>(future, listener); } else { // If a listener is supplied we do not want to create a Task return new Pair<>(null, optListener); diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java index 9057905ca..f3b6e5cb4 100644 --- a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java +++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java @@ -34,14 +34,13 @@ import com.google.common.io.ByteStreams; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseRequestInitializer; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; -import com.google.firebase.internal.TaskToApiFuture; -import com.google.firebase.tasks.Task; +import java.io.IOException; import java.util.Map; -import java.util.concurrent.Callable; /** * This class is the entry point for all server-side Firebase Instance ID actions. @@ -118,37 +117,58 @@ void setInterceptor(HttpResponseInterceptor interceptor) { * pursuant to the General Data Protection Regulation (GDPR). * * @param instanceId A non-null, non-empty instance ID string. + * @throws IllegalArgumentException If the instance ID is null or empty. + * @throws FirebaseInstanceIdException If an error occurs while deleting the instance ID. + */ + public void deleteInstanceId(@NonNull String instanceId) throws FirebaseInstanceIdException { + deleteInstanceIdOp(instanceId).call(); + } + + /** + * Similar to {@link #deleteInstanceId(String)} but performs the operation asynchronously. + * + * @param instanceId A non-null, non-empty instance ID string. * @return An {@code ApiFuture} which will complete successfully when the instance ID is deleted, - * or unsuccessfully with the failure Exception.. + * or unsuccessfully with the failure Exception. + * @throws IllegalArgumentException If the instance ID is null or empty. */ public ApiFuture deleteInstanceIdAsync(@NonNull String instanceId) { - return new TaskToApiFuture<>(deleteInstanceId(instanceId)); + return deleteInstanceIdOp(instanceId).callAsync(app); } - private Task deleteInstanceId(final String instanceId) { + private CallableOperation deleteInstanceIdOp( + final String instanceId) { checkArgument(!Strings.isNullOrEmpty(instanceId), "instance ID must not be null or empty"); - return ImplFirebaseTrampolines.submitCallable(app, new Callable(){ + return new CallableOperation() { @Override - public Void call() throws Exception { + protected Void execute() throws FirebaseInstanceIdException { String url = String.format( "%s/project/%s/instanceId/%s", IID_SERVICE_URL, projectId, instanceId); - HttpRequest request = requestFactory.buildDeleteRequest(new GenericUrl(url)); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(interceptor); HttpResponse response = null; try { + HttpRequest request = requestFactory.buildDeleteRequest(new GenericUrl(url)); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(interceptor); response = request.execute(); ByteStreams.exhaust(response.getContent()); } catch (Exception e) { handleError(instanceId, e); } finally { - if (response != null) { - response.disconnect(); - } + disconnectQuietly(response); } return null; } - }); + }; + } + + private static void disconnectQuietly(HttpResponse response) { + if (response != null) { + try { + response.disconnect(); + } catch (IOException ignored) { + // ignored + } + } } private void handleError(String instanceId, Exception e) throws FirebaseInstanceIdException { diff --git a/src/main/java/com/google/firebase/internal/CallableOperation.java b/src/main/java/com/google/firebase/internal/CallableOperation.java new file mode 100644 index 000000000..ac28e8a7c --- /dev/null +++ b/src/main/java/com/google/firebase/internal/CallableOperation.java @@ -0,0 +1,49 @@ +/* + * 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.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.core.ApiFuture; +import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; +import java.util.concurrent.Callable; + +/** + * An operation that can be invoked synchronously or asynchronously. Subclasses can specify + * the return type and a specific exception type to be thrown. + */ +public abstract class CallableOperation implements Callable { + + protected abstract T execute() throws V; + + @Override + public final T call() throws V { + return execute(); + } + + /** + * Run this operation asynchronously on the main thread pool of the specified {@link FirebaseApp}. + * + * @param app A non-null {@link FirebaseApp}. + * @return An {@code ApiFuture}. + */ + public final ApiFuture callAsync(@NonNull FirebaseApp app) { + checkNotNull(app); + return ImplFirebaseTrampolines.submitCallable(app, this); + } +} diff --git a/src/main/java/com/google/firebase/internal/FirebaseScheduledExecutor.java b/src/main/java/com/google/firebase/internal/FirebaseScheduledExecutor.java new file mode 100644 index 000000000..0a472ba47 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/FirebaseScheduledExecutor.java @@ -0,0 +1,60 @@ +/* + * 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.internal; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; + +/** + * A single-threaded scheduled executor implementation. Allows naming the threads, and spawns + * new threads as daemons. + */ +public class FirebaseScheduledExecutor extends ScheduledThreadPoolExecutor { + + public FirebaseScheduledExecutor(@NonNull ThreadFactory threadFactory, @NonNull String name) { + this(threadFactory, name, null); + } + + public FirebaseScheduledExecutor( + @NonNull ThreadFactory threadFactory, @NonNull String name, + @Nullable Thread.UncaughtExceptionHandler handler) { + super(1, decorateThreadFactory(threadFactory, name, handler)); + setRemoveOnCancelPolicy(true); + } + + static ThreadFactory getThreadFactoryWithName( + @NonNull ThreadFactory threadFactory, @NonNull String name) { + return decorateThreadFactory(threadFactory, name, null); + } + + private static ThreadFactory decorateThreadFactory( + ThreadFactory threadFactory, String name, Thread.UncaughtExceptionHandler handler) { + checkArgument(!Strings.isNullOrEmpty(name)); + ThreadFactoryBuilder builder = new ThreadFactoryBuilder() + .setThreadFactory(threadFactory) + .setNameFormat(name) + .setDaemon(true); + if (handler != null) { + builder.setUncaughtExceptionHandler(handler); + } + return builder.build(); + } +} diff --git a/src/main/java/com/google/firebase/internal/FirebaseThreadManagers.java b/src/main/java/com/google/firebase/internal/FirebaseThreadManagers.java index 0e503ba36..877450fcc 100644 --- a/src/main/java/com/google/firebase/internal/FirebaseThreadManagers.java +++ b/src/main/java/com/google/firebase/internal/FirebaseThreadManagers.java @@ -16,9 +16,6 @@ package com.google.firebase.internal; -import static com.google.common.base.Preconditions.checkState; - -import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.firebase.FirebaseApp; import com.google.firebase.ThreadManager; @@ -36,15 +33,7 @@ public class FirebaseThreadManagers { private static final Logger logger = LoggerFactory.getLogger(FirebaseThreadManagers.class); - public static final ThreadManager DEFAULT_THREAD_MANAGER; - - static { - if (GaeThreadFactory.isAvailable()) { - DEFAULT_THREAD_MANAGER = new GaeThreadManager(); - } else { - DEFAULT_THREAD_MANAGER = new DefaultThreadManager(); - } - } + public static final ThreadManager DEFAULT_THREAD_MANAGER = new DefaultThreadManager(); /** * An abstract ThreadManager implementation that uses the same executor service @@ -93,12 +82,8 @@ private static class DefaultThreadManager extends GlobalThreadManager { @Override protected ExecutorService doInit() { - // Create threads as daemons to ensure JVM exit when all foreground jobs are complete. - ThreadFactory threadFactory = new ThreadFactoryBuilder() - .setNameFormat("firebase-default-%d") - .setDaemon(true) - .setThreadFactory(getThreadFactory()) - .build(); + ThreadFactory threadFactory = FirebaseScheduledExecutor.getThreadFactoryWithName( + getThreadFactory(), "firebase-default-%d"); return Executors.newCachedThreadPool(threadFactory); } @@ -113,52 +98,4 @@ protected ThreadFactory getThreadFactory() { return Executors.defaultThreadFactory(); } } - - /** - * The ThreadManager implementation that will be used by default in the Google App Engine - * environment. - * - *

    Auto-scaling: Creates an ExecutorService backed by the request-scoped ThreadFactory. This - * can be used for any short-lived task, such as the ones submitted by components like - * FirebaseAuth. {@link #getThreadFactory()} throws an exception, since long-lived threads - * cannot be supported. Therefore task scheduling and RTDB will not work. - * - *

    Manual-scaling: Creates a single-threaded ExecutorService backed by the background - * ThreadFactory. Keeps the threads alive indefinitely by periodically restarting them (see - * {@link RevivingScheduledExecutor}). Threads will be terminated only when the method - * {@link #releaseExecutor(FirebaseApp, ExecutorService)} is invoked. The - * {@link #getThreadFactory()} also returns the background ThreadFactory enabling other - * components in the SDK to start long-lived threads when necessary. Therefore task scheduling - * and RTDB can be supported as if running on the regular JVM. - * - *

    Basic-scaling: Behavior is similar to manual-scaling. Since the threads are kept alive - * indefinitely, prevents the GAE idle instance shutdown. Developers are advised to use - * a custom ThreadManager implementation if idle instance shutdown should be supported. In - * general, a ThreadManager implementation that uses the request-scoped ThreadFactory, or the - * background ThreadFactory with specific keep-alive times can easily facilitate GAE idle - * instance shutdown. Note that this often comes at the cost of losing scheduled tasks and RTDB - * support. Therefore, for these features, manual-scaling is the recommended GAE deployment mode - * regardless of the ThreadManager implementation used. - */ - private static class GaeThreadManager extends GlobalThreadManager { - - @Override - protected ExecutorService doInit() { - return new GaeExecutorService("gae-firebase-default"); - } - - @Override - protected void doCleanup(ExecutorService executorService) { - executorService.shutdownNow(); - } - - @Override - protected ThreadFactory getThreadFactory() { - GaeThreadFactory threadFactory = GaeThreadFactory.getInstance(); - checkState(threadFactory.isUsingBackgroundThreads(), - "Failed to initialize a GAE background thread factory. Background thread support " - + "is required to create long-lived threads."); - return threadFactory; - } - } } diff --git a/src/main/java/com/google/firebase/internal/GaeExecutorService.java b/src/main/java/com/google/firebase/internal/GaeExecutorService.java deleted file mode 100644 index 6d1b323e8..000000000 --- a/src/main/java/com/google/firebase/internal/GaeExecutorService.java +++ /dev/null @@ -1,195 +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.internal; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; - -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; - -import java.util.Collection; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.SynchronousQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicReference; - -/** - * An ExecutorService instance that can operate in the Google App Engine environment. When - * available, uses background thread support to initialize an ExecutorService with long-lived - * threads. Otherwise, creates an ExecutorService that spawns short-lived threads as tasks - * are submitted. The actual ExecutorService implementation is lazy-loaded to prevent making - * unnecessary RPC calls to the GAE's native ThreadFactory mechanism. - */ -class GaeExecutorService implements ExecutorService { - - private final AtomicReference executor = new AtomicReference<>(); - private final String threadName; - private final ThreadFactory threadFactory; - private boolean shutdown; - - GaeExecutorService(String threadName) { - this(threadName, GaeThreadFactory.getInstance()); - } - - GaeExecutorService(String threadName, ThreadFactory threadFactory) { - checkArgument(!Strings.isNullOrEmpty(threadName)); - this.threadName = threadName; - this.threadFactory = threadFactory; - } - - private ExecutorService ensureExecutorService() { - ExecutorService executorService = executor.get(); - if (executorService == null) { - synchronized (executor) { - checkState(!shutdown); - executorService = executor.get(); - if (executorService == null) { - executorService = newExecutorService(threadFactory, threadName); - executor.compareAndSet(null, executorService); - } - } - } - return executorService; - } - - @Override - public Future submit(Callable task) { - return ensureExecutorService().submit(task); - } - - @Override - public Future submit(Runnable task, T result) { - return ensureExecutorService().submit(task, result); - } - - @Override - public Future submit(Runnable task) { - return ensureExecutorService().submit(task); - } - - @Override - public List> invokeAll(Collection> tasks) - throws InterruptedException { - return ensureExecutorService().invokeAll(tasks); - } - - @Override - public List> invokeAll( - Collection> tasks, long timeout, TimeUnit unit) - throws InterruptedException { - return ensureExecutorService().invokeAll(tasks, timeout, unit); - } - - @Override - public T invokeAny(Collection> tasks) - throws InterruptedException, ExecutionException { - return ensureExecutorService().invokeAny(tasks); - } - - @Override - public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) - throws InterruptedException, ExecutionException, TimeoutException { - return ensureExecutorService().invokeAny(tasks, timeout, unit); - } - - @Override - public void shutdown() { - synchronized (executor) { - ExecutorService executorService = executor.get(); - if (executorService != null && !shutdown) { - executorService.shutdown(); - } - shutdown = true; - } - } - - @Override - public List shutdownNow() { - synchronized (executor) { - ExecutorService executorService = executor.get(); - List result; - if (executorService != null && !shutdown) { - result = executorService.shutdownNow(); - } else { - result = ImmutableList.of(); - } - shutdown = true; - return result; - } - } - - @Override - public boolean isShutdown() { - synchronized (executor) { - return shutdown; - } - } - - @Override - public boolean isTerminated() { - synchronized (executor) { - if (!shutdown) { - return false; - } - ExecutorService executorService = executor.get(); - return executorService == null || executorService.isTerminated(); - } - } - - @Override - public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { - ExecutorService executorService; - synchronized (executor) { - executorService = executor.get(); - } - // call await outside the lock - return executorService == null || executorService.awaitTermination(timeout, unit); - } - - @Override - public void execute(Runnable command) { - ensureExecutorService().execute(command); - } - - private static ExecutorService newExecutorService( - ThreadFactory threadFactory, String threadName) { - boolean background = threadFactory instanceof GaeThreadFactory - && ((GaeThreadFactory) threadFactory).isUsingBackgroundThreads(); - if (background) { - // Create a thread pool with long-lived threads if background thread support is available. - return new RevivingScheduledExecutor(threadFactory, threadName, true); - } else { - // Create an executor that creates a new thread for each submitted task, when background - // thread support is not available. - return new ThreadPoolExecutor( - 0, - Integer.MAX_VALUE, - 0L, - TimeUnit.SECONDS, - new SynchronousQueue(), - threadFactory); - } - } -} diff --git a/src/main/java/com/google/firebase/internal/GaeThreadFactory.java b/src/main/java/com/google/firebase/internal/GaeThreadFactory.java deleted file mode 100644 index 3791dad78..000000000 --- a/src/main/java/com/google/firebase/internal/GaeThreadFactory.java +++ /dev/null @@ -1,165 +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.internal; - -import static com.google.common.base.Preconditions.checkNotNull; - -import java.lang.reflect.InvocationTargetException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * GaeThreadFactory is a thread factory that works on App Engine. It uses background threads on - * manually-scaled GAE backends and request-scoped threads on automatically scaled instances. - * - *

    This class is thread-safe. - */ -public class GaeThreadFactory implements ThreadFactory { - - private static final Logger logger = LoggerFactory.getLogger(GaeThreadFactory.class); - - public static final ExecutorService DEFAULT_EXECUTOR = - new GaeExecutorService("LegacyFirebaseDefault"); - private static final String GAE_THREAD_MANAGER_CLASS = "com.google.appengine.api.ThreadManager"; - private static final GaeThreadFactory instance = new GaeThreadFactory(); - private final AtomicReference threadFactory = new AtomicReference<>(null); - - private GaeThreadFactory() {} - - public static GaeThreadFactory getInstance() { - return instance; - } - - /** Returns whether GaeThreadFactory can be used on this system (true for GAE). */ - public static boolean isAvailable() { - try { - Class.forName(GAE_THREAD_MANAGER_CLASS); - return System.getProperty("com.google.appengine.runtime.environment") != null; - } catch (ClassNotFoundException e) { - return false; - } - } - - private static ThreadFactory createBackgroundFactory() - throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, - IllegalAccessException { - Class gaeThreadManager = Class.forName(GAE_THREAD_MANAGER_CLASS); - return (ThreadFactory) gaeThreadManager.getMethod("backgroundThreadFactory").invoke(null); - } - - private static ThreadFactory createRequestScopedFactory() - throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, - IllegalAccessException { - Class gaeThreadManager = Class.forName(GAE_THREAD_MANAGER_CLASS); - return (ThreadFactory) gaeThreadManager.getMethod("currentRequestThreadFactory").invoke(null); - } - - @Override - public Thread newThread(Runnable r) { - ThreadFactoryWrapper wrapper = threadFactory.get(); - if (wrapper != null) { - return wrapper.getThreadFactory().newThread(r); - } - return initThreadFactory(r); - } - - /** - * Checks whether background thread support is available in the current environment. This method - * forces the ThreadFactory to get fully initialized (if not already initialized), by running a - * no-op thread. - * - * @return true if background thread support is available, and false otherwise. - */ - boolean isUsingBackgroundThreads() { - ThreadFactoryWrapper wrapper = threadFactory.get(); - if (wrapper != null) { - return wrapper.isUsingBackgroundThreads(); - } - - // Create a no-op thread to force initialize the ThreadFactory implementation. - // Start the resulting thread, since GAE code seems to expect that. - initThreadFactory(new Runnable() { - @Override - public void run() {} - }).start(); - return threadFactory.get().isUsingBackgroundThreads(); - } - - private Thread initThreadFactory(Runnable r) { - ThreadFactory threadFactory; - boolean usesBackgroundThreads = false; - Thread thread; - // Since we can't tell manually-scaled GAE instances apart until we spawn a thread (which - // sends an RPC and thus is done after class initialization), we initialize both of GAE's - // thread factories here and discard one once we detect that we are running in an - // automatically scaled instance. - // - // Note: It's fine if multiple threads access this block at the same time. - try { - try { - threadFactory = createBackgroundFactory(); - thread = threadFactory.newThread(r); - usesBackgroundThreads = true; - } catch (IllegalStateException e) { - logger.info("Falling back to GAE's request-scoped threads. Firebase requires " - + "manually-scaled instances for most operations."); - threadFactory = createRequestScopedFactory(); - thread = threadFactory.newThread(r); - } - } catch (ClassNotFoundException - | InvocationTargetException - | NoSuchMethodException - | IllegalAccessException e) { - threadFactory = - new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - logger.warn("Failed to initialize native GAE thread factory. " - + "GaeThreadFactory cannot be used in a non-GAE environment."); - return null; - } - }; - thread = null; - } - - ThreadFactoryWrapper wrapper = new ThreadFactoryWrapper(threadFactory, usesBackgroundThreads); - this.threadFactory.compareAndSet(null, wrapper); - return thread; - } - - private static class ThreadFactoryWrapper { - - private final ThreadFactory threadFactory; - private final boolean usingBackgroundThreads; - - private ThreadFactoryWrapper(ThreadFactory threadFactory, boolean usingBackgroundThreads) { - this.threadFactory = checkNotNull(threadFactory); - this.usingBackgroundThreads = usingBackgroundThreads; - } - - ThreadFactory getThreadFactory() { - return threadFactory; - } - - boolean isUsingBackgroundThreads() { - return usingBackgroundThreads; - } - } -} diff --git a/src/main/java/com/google/firebase/tasks/RuntimeExecutionException.java b/src/main/java/com/google/firebase/internal/ListenableFuture2ApiFuture.java similarity index 54% rename from src/main/java/com/google/firebase/tasks/RuntimeExecutionException.java rename to src/main/java/com/google/firebase/internal/ListenableFuture2ApiFuture.java index de460dd9b..18d98da7e 100644 --- a/src/main/java/com/google/firebase/tasks/RuntimeExecutionException.java +++ b/src/main/java/com/google/firebase/internal/ListenableFuture2ApiFuture.java @@ -14,16 +14,19 @@ * limitations under the License. */ -package com.google.firebase.tasks; +package com.google.firebase.internal; + +import com.google.api.core.ApiFuture; +import com.google.common.util.concurrent.ForwardingListenableFuture.SimpleForwardingListenableFuture; +import com.google.common.util.concurrent.ListenableFuture; /** - * Runtime version of {@link java.util.concurrent.ExecutionException}. - * - * @see Task#getResult(Class) + * Adapter from Guava ListenableFuture to GAX ApiFuture. */ -public class RuntimeExecutionException extends RuntimeException { +public class ListenableFuture2ApiFuture extends SimpleForwardingListenableFuture implements + ApiFuture { - public RuntimeExecutionException(Throwable cause) { - super(cause); + public ListenableFuture2ApiFuture(ListenableFuture delegate) { + super(delegate); } } diff --git a/src/main/java/com/google/firebase/internal/RevivingScheduledExecutor.java b/src/main/java/com/google/firebase/internal/RevivingScheduledExecutor.java deleted file mode 100644 index efa989943..000000000 --- a/src/main/java/com/google/firebase/internal/RevivingScheduledExecutor.java +++ /dev/null @@ -1,213 +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.internal; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.annotations.VisibleForTesting; - -import java.security.AccessControlException; -import java.util.concurrent.Callable; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.RunnableScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * RevivingScheduledExecutor is an implementation of ScheduledThreadPoolExecutor that uses one - * periodically restarting worker thread as its work queue. This allows customers of this class to - * use this executor on App Engine despite App Engine's thread-lifetime limitations. - */ -public class RevivingScheduledExecutor extends ScheduledThreadPoolExecutor { - - private static final Logger logger = LoggerFactory.getLogger(RevivingScheduledExecutor.class); - - /** Exception to throw to shut down the core threads. */ - private static final RuntimeException REVIVE_THREAD_EXCEPTION = new RuntimeException( - "Restarting Firebase Worker Thread. This exception is expected to occur periodically " - + "when deployed in the App Engine environment, and can be ignored."); - - /** The lifetime of a thread. Maximum lifetime of a thread on GAE is 24 hours. */ - private static final long PERIODIC_RESTART_INTERVAL_MS = TimeUnit.HOURS.toMillis(12); - - /** - * Time by which we offset restarts to ensure that not all threads die at the same time. This is - * meant to decrease cross-thread liveliness issues during restarts. - */ - private static final long PERIODIC_RESTART_OFFSET_MS = TimeUnit.MINUTES.toMillis(5); - - private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(0); - - private final long initialDelayMs; - private final long timeoutMs; - - // Flag set before throwing a REVIVE_THREAD_EXCEPTION and unset once a new thread has been - // created. Used to call afterRestart() appropriately. - private AtomicBoolean requestedRestart = new AtomicBoolean(); - - /** - * Creates a new RevivingScheduledExecutor that optionally restarts its worker thread every twelve - * hours. - * - * @param threadFactory Thread factory to use to restart threads. - * @param threadName Name of the threads in the pool. - * @param periodicRestart Periodically restart its worked threads. - */ - public RevivingScheduledExecutor( - final ThreadFactory threadFactory, final String threadName, final boolean periodicRestart) { - this( - threadFactory, - threadName, - periodicRestart ? PERIODIC_RESTART_OFFSET_MS * INSTANCE_COUNTER.get() : 0, - periodicRestart ? PERIODIC_RESTART_INTERVAL_MS : -1); - } - - @VisibleForTesting - RevivingScheduledExecutor( - final ThreadFactory threadFactory, - final String threadName, - final long initialDelayMs, - final long timeoutMs) { - super(0); - checkNotNull(threadFactory, "threadFactory must not be null"); - INSTANCE_COUNTER.incrementAndGet(); - this.initialDelayMs = initialDelayMs; - this.timeoutMs = timeoutMs; - setRemoveOnCancelPolicy(true); - setThreadFactory( - new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - logger.debug("Creating new thread for: {}", threadName); - Thread thread = threadFactory.newThread(r); - try { - thread.setName(threadName); - thread.setDaemon(true); - } catch (AccessControlException ignore) { - // Unsupported on App Engine. - } - if (requestedRestart.getAndSet(false)) { - afterRestart(); - } - return thread; - } - }); - } - - @Override - public void execute(Runnable runnable) { - // This gets called when the execute() method from Executor is directly invoked. - ensureRunning(); - super.execute(runnable); - } - - @Override - protected RunnableScheduledFuture decorateTask( - Runnable runnable, RunnableScheduledFuture task) { - // This gets called by ScheduledThreadPoolExecutor before scheduling a Runnable. - ensureRunning(); - return task; - } - - @Override - protected RunnableScheduledFuture decorateTask( - Callable callable, RunnableScheduledFuture task) { - // This gets called by ScheduledThreadPoolExecutor before scheduling a Callable. - ensureRunning(); - return task; - } - - @Override - protected void afterExecute(Runnable runnable, Throwable throwable) { - super.afterExecute(runnable, throwable); - if (throwable == null && runnable instanceof Future) { - Future future = (Future) runnable; - try { - // Not all Futures will be done, e.g. when used with scheduledAtFixedRate - if (future.isDone()) { - future.get(); - } - } catch (CancellationException ce) { - // Cancellation exceptions are okay, we expect them to happen sometimes - } catch (ExecutionException ee) { - throwable = ee.getCause(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - if (throwable == REVIVE_THREAD_EXCEPTION) { - // Re-throwing this exception will kill the thread and cause - // ScheduledThreadPoolExecutor to - // spawn a new thread. - throw (RuntimeException) throwable; - } else if (throwable != null) { - handleException(throwable); - } - } - - /** - * Called when an exception occurs during execution of a Runnable/Callable. The default - * implementation does nothing. - */ - protected void handleException(Throwable throwable) {} - - /** Called before the worker thread gets shutdown before a restart. */ - protected void beforeRestart() {} - - /** Called after the worker thread got recreated after a restart. */ - protected void afterRestart() {} - - private synchronized void ensureRunning() { - if (getCorePoolSize() == 0) { - setCorePoolSize(1); - schedulePeriodicShutdown(); - } - } - - private void schedulePeriodicShutdown() { - if (timeoutMs >= 0) { - @SuppressWarnings("unused") - Future possiblyIgnoredError = - schedule( - new Runnable() { - @Override - public void run() { - // We have to manually reschedule this task here as periodic tasks get - // cancelled after - // throwing exceptions. - @SuppressWarnings("unused") - Future possiblyIgnoredError1 = - RevivingScheduledExecutor.this.schedule( - this, timeoutMs, TimeUnit.MILLISECONDS); - requestedRestart.set(true); - beforeRestart(); - throw REVIVE_THREAD_EXCEPTION; - } - }, - initialDelayMs + timeoutMs, - TimeUnit.MILLISECONDS); - } - } -} diff --git a/src/main/java/com/google/firebase/internal/TaskToApiFuture.java b/src/main/java/com/google/firebase/internal/TaskToApiFuture.java deleted file mode 100644 index 238ba43a3..000000000 --- a/src/main/java/com/google/firebase/internal/TaskToApiFuture.java +++ /dev/null @@ -1,82 +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.internal; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.api.core.ApiFuture; -import com.google.firebase.tasks.OnCompleteListener; -import com.google.firebase.tasks.Task; -import com.google.firebase.tasks.Tasks; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -/** - * An ApiFuture implementation that wraps a {@link Task}. This is an interim solution that enables - * us to expose Tasks as ApiFutures, until we fully remove the Task API. - * - * @param Type of the result produced by this Future. - */ -public class TaskToApiFuture implements ApiFuture { - - private final Task task; - private boolean cancelled; - - public TaskToApiFuture(Task task) { - this.task = checkNotNull(task, "task must not be null"); - } - - @Override - public void addListener(final Runnable runnable, Executor executor) { - task.addOnCompleteListener(executor, new OnCompleteListener() { - @Override - public void onComplete(Task task) { - runnable.run(); - } - }); - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - // Cannot be supported with Tasks - cancelled = true; - return false; - } - - @Override - public boolean isCancelled() { - return false; - } - - @Override - public boolean isDone() { - return cancelled || task.isComplete(); - } - - @Override - public T get() throws InterruptedException, ExecutionException { - return Tasks.await(task); - } - - @Override - public T get(long timeout, @NonNull TimeUnit unit) - throws InterruptedException, ExecutionException, TimeoutException { - return Tasks.await(task, timeout, unit); - } -} diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index e303ac863..83bed681c 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -38,15 +38,13 @@ 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 com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; -import com.google.firebase.internal.TaskToApiFuture; -import com.google.firebase.tasks.Task; import java.io.IOException; import java.util.List; import java.util.Map; -import java.util.concurrent.Callable; /** * This class is the entry point for all server-side Firebase Cloud Messaging actions. @@ -139,11 +137,10 @@ public static synchronized FirebaseMessaging getInstance(FirebaseApp app) { * Sends the given {@link Message} via Firebase Cloud Messaging. * * @param message A non-null {@link Message} to be sent. - * @return An {@code ApiFuture} that will complete with a message ID string when the message - * has been sent. + * @return A message ID string. */ - public ApiFuture sendAsync(@NonNull Message message) { - return sendAsync(message, false); + public String send(@NonNull Message message) throws FirebaseMessagingException { + return send(message, false); } /** @@ -154,25 +151,50 @@ public ApiFuture sendAsync(@NonNull Message message) { * * @param message A non-null {@link Message} to be sent. * @param dryRun a boolean indicating whether to perform a dry run (validation only) of the send. + * @return A message ID string. + */ + public String send(@NonNull Message message, boolean dryRun) throws FirebaseMessagingException { + return sendOp(message, dryRun).call(); + } + + /** + * Similar to {@link #send(Message)} but performs the operation asynchronously. + * + * @param message A non-null {@link Message} to be sent. + * @return An {@code ApiFuture} that will complete with a message ID string when the message + * has been sent. + */ + public ApiFuture sendAsync(@NonNull Message message) { + return sendAsync(message, false); + } + + /** + * Similar to {@link #send(Message, boolean)} but performs the operation asynchronously. + * + * @param message A non-null {@link Message} to be sent. + * @param dryRun a boolean indicating whether to perform a dry run (validation only) of the send. * @return An {@code ApiFuture} that will complete with a message ID string when the message * has been sent, or when the emulation has finished. */ public ApiFuture sendAsync(@NonNull Message message, boolean dryRun) { - return new TaskToApiFuture<>(send(message, dryRun)); + return sendOp(message, dryRun).callAsync(app); } - private Task send(final Message message, final boolean dryRun) { - checkNotNull(message, "message must not be null"); - return ImplFirebaseTrampolines.submitCallable(app, new Callable() { - @Override - public String call() throws FirebaseMessagingException { - return makeSendRequest(message, dryRun); - } - }); + /** + * Subscribes a list of registration tokens to a topic. + * + * @param registrationTokens A non-null, non-empty list of device registration tokens, with at + * most 1000 entries. + * @param topic Name of the topic to subscribe to. May contain the {@code /topics/} prefix. + * @return A {@link TopicManagementResponse}. + */ + public TopicManagementResponse subscribeToTopic(@NonNull List registrationTokens, + @NonNull String topic) throws FirebaseMessagingException { + return manageTopicOp(registrationTokens, topic, IID_SUBSCRIBE_PATH).call(); } /** - * Subscribes a list of registration tokens to a topic. + * Similar to {@link #subscribeToTopic(List, String)} but performs the operation asynchronously. * * @param registrationTokens A non-null, non-empty list of device registration tokens, with at * most 1000 entries. @@ -181,24 +203,25 @@ public String call() throws FirebaseMessagingException { */ public ApiFuture subscribeToTopicAsync( @NonNull List registrationTokens, @NonNull String topic) { - return new TaskToApiFuture<>(subscribeToTopic(registrationTokens, topic)); + return manageTopicOp(registrationTokens, topic, IID_SUBSCRIBE_PATH).callAsync(app); } - private Task subscribeToTopic( - final List registrationTokens, final String topic) { - checkRegistrationTokens(registrationTokens); - checkTopic(topic); - - return ImplFirebaseTrampolines.submitCallable(app, new Callable() { - @Override - public TopicManagementResponse call() throws FirebaseMessagingException { - return makeTopicManagementRequest(registrationTokens, topic, IID_SUBSCRIBE_PATH); - } - }); + /** + * Unubscribes a list of registration tokens from a topic. + * + * @param registrationTokens A non-null, non-empty list of device registration tokens, with at + * most 1000 entries. + * @param topic Name of the topic to unsubscribe from. May contain the {@code /topics/} prefix. + * @return A {@link TopicManagementResponse}. + */ + public TopicManagementResponse unsubscribeFromTopic(@NonNull List registrationTokens, + @NonNull String topic) throws FirebaseMessagingException { + return manageTopicOp(registrationTokens, topic, IID_UNSUBSCRIBE_PATH).call(); } /** - * Unubscribes a list of registration tokens from a topic. + * Similar to {@link #unsubscribeFromTopic(List, String)} but performs the operation + * asynchronously. * * @param registrationTokens A non-null, non-empty list of device registration tokens, with at * most 1000 entries. @@ -207,48 +230,42 @@ public TopicManagementResponse call() throws FirebaseMessagingException { */ public ApiFuture unsubscribeFromTopicAsync( @NonNull List registrationTokens, @NonNull String topic) { - return new TaskToApiFuture<>(unsubscribeFromTopic(registrationTokens, topic)); + return manageTopicOp(registrationTokens, topic, IID_UNSUBSCRIBE_PATH) + .callAsync(app); } - private Task unsubscribeFromTopic( - final List registrationTokens, final String topic) { - checkRegistrationTokens(registrationTokens); - checkTopic(topic); - - return ImplFirebaseTrampolines.submitCallable(app, new Callable() { + private CallableOperation sendOp( + final Message message, final boolean dryRun) { + checkNotNull(message, "message must not be null"); + return new CallableOperation() { @Override - public TopicManagementResponse call() throws FirebaseMessagingException { - return makeTopicManagementRequest(registrationTokens, topic, IID_UNSUBSCRIBE_PATH); + protected String execute() throws FirebaseMessagingException { + ImmutableMap.Builder payload = ImmutableMap.builder() + .put("message", message); + if (dryRun) { + payload.put("validate_only", true); + } + HttpResponse response = null; + try { + HttpRequest request = requestFactory.buildPostRequest( + new GenericUrl(url), new JsonHttpContent(jsonFactory, payload.build())); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(interceptor); + response = request.execute(); + MessagingServiceResponse parsed = new MessagingServiceResponse(); + jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); + return parsed.name; + } catch (HttpResponseException e) { + handleSendHttpError(e); + return null; + } catch (IOException e) { + throw new FirebaseMessagingException( + INTERNAL_ERROR, "Error while calling FCM backend service", e); + } finally { + disconnectQuietly(response); + } } - }); - } - - private String makeSendRequest(Message message, - boolean dryRun) throws FirebaseMessagingException { - ImmutableMap.Builder payload = ImmutableMap.builder() - .put("message", message); - if (dryRun) { - payload.put("validate_only", true); - } - HttpResponse response = null; - try { - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(url), new JsonHttpContent(jsonFactory, payload.build())); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(interceptor); - response = request.execute(); - MessagingServiceResponse parsed = new MessagingServiceResponse(); - jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); - return parsed.name; - } catch (HttpResponseException e) { - handleSendHttpError(e); - return null; - } catch (IOException e) { - throw new FirebaseMessagingException( - INTERNAL_ERROR, "Error while calling FCM backend service", e); - } finally { - disconnectQuietly(response); - } + }; } private void handleSendHttpError(HttpResponseException e) throws FirebaseMessagingException { @@ -273,39 +290,50 @@ private void handleSendHttpError(HttpResponseException e) throws FirebaseMessagi throw new FirebaseMessagingException(code, msg, e); } - private TopicManagementResponse makeTopicManagementRequest(List registrationTokens, - String topic, String path) throws FirebaseMessagingException { - if (!topic.startsWith("/topics/")) { - topic = "/topics/" + topic; - } - Map payload = ImmutableMap.of( - "to", topic, - "registration_tokens", registrationTokens - ); - - final String url = String.format("%s/%s", IID_HOST, path); - HttpResponse response = null; - try { - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); - request.getHeaders().set("access_token_auth", "true"); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(interceptor); - response = request.execute(); - InstanceIdServiceResponse parsed = new InstanceIdServiceResponse(); - jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); - checkState(parsed.results != null && !parsed.results.isEmpty(), - "unexpected response from topic management service"); - return new TopicManagementResponse(parsed.results); - } catch (HttpResponseException e) { - handleTopicManagementHttpError(e); - return null; - } catch (IOException e) { - throw new FirebaseMessagingException( - INTERNAL_ERROR, "Error while calling IID backend service", e); - } finally { - disconnectQuietly(response); - } + private CallableOperation + manageTopicOp( + final List registrationTokens, final String topic, final String path) { + checkRegistrationTokens(registrationTokens); + checkTopic(topic); + return new CallableOperation() { + @Override + protected TopicManagementResponse execute() throws FirebaseMessagingException { + final String prefixedTopic; + if (topic.startsWith("/topics/")) { + prefixedTopic = topic; + } else { + prefixedTopic = "/topics/" + topic; + } + Map payload = ImmutableMap.of( + "to", prefixedTopic, + "registration_tokens", registrationTokens + ); + + final String url = String.format("%s/%s", IID_HOST, path); + HttpResponse response = null; + try { + HttpRequest request = requestFactory.buildPostRequest( + new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); + request.getHeaders().set("access_token_auth", "true"); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(interceptor); + response = request.execute(); + InstanceIdServiceResponse parsed = new InstanceIdServiceResponse(); + jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); + checkState(parsed.results != null && !parsed.results.isEmpty(), + "unexpected response from topic management service"); + return new TopicManagementResponse(parsed.results); + } catch (HttpResponseException e) { + handleTopicManagementHttpError(e); + return null; + } catch (IOException e) { + throw new FirebaseMessagingException( + INTERNAL_ERROR, "Error while calling IID backend service", e); + } finally { + disconnectQuietly(response); + } + } + }; } private void handleTopicManagementHttpError( diff --git a/src/main/java/com/google/firebase/tasks/Continuation.java b/src/main/java/com/google/firebase/tasks/Continuation.java deleted file mode 100644 index ef76b54e3..000000000 --- a/src/main/java/com/google/firebase/tasks/Continuation.java +++ /dev/null @@ -1,77 +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.tasks; - -import com.google.firebase.internal.NonNull; - -/** - * A function that is called to continue execution after completion of a {@link Task}. - * - * @param the Task's result type - * @param the Continuation's result type - * @see Task#continueWith(Continuation) - * @see Task#continueWithTask(Continuation) - */ -public interface Continuation { - - /** - * Returns the result of applying this Continuation to {@code task}. - * - *

    To propagate failure from the completed Task call {@link Task#getResult()} and allow the - * {@link RuntimeExecutionException} to propagate. The RuntimeExecutionException will be unwrapped - * such that the Task returned by {@link Task#continueWith(Continuation)} or {@link - * Task#continueWithTask(Continuation)} fails with the original exception. - * - *

    To suppress specific failures call {@link Task#getResult(Class)} and catch the exception - * types of interest: - * - *

    -   * task.continueWith(new Continuation<String, String>() {
    -   * {@literal @}Override
    -   * public String then(Task<String> task) {
    -   * try {
    -   * return task.getResult(IOException.class);
    -   * } catch (FileNotFoundException e) {
    -   * return "Not found";
    -   * } catch (IOException e) {
    -   * return "Read failed";
    -   * }
    -   * }
    -   * }
    -   * 
    - * - *

    To suppress all failures guard any calls to {@link Task#getResult()} with {@link - * Task#isSuccessful()}: - * - *

    -   * task.continueWith(new Continuation<String, String>() {
    -   * {@literal @}Override
    -   * public String then(Task<String> task) {
    -   * if (task.isSuccessful()) {
    -   * return task.getResult();
    -   * } else {
    -   * return DEFAULT_VALUE;
    -   * }
    -   * }
    -   * }
    -   * 
    - * - * @param task the completed Task. Never null - * @throws Exception if the result couldn't be produced - */ - R then(@NonNull Task task) throws Exception; -} diff --git a/src/main/java/com/google/firebase/tasks/ContinueWithCompletionListener.java b/src/main/java/com/google/firebase/tasks/ContinueWithCompletionListener.java deleted file mode 100644 index 19c621368..000000000 --- a/src/main/java/com/google/firebase/tasks/ContinueWithCompletionListener.java +++ /dev/null @@ -1,71 +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.tasks; - -import com.google.firebase.internal.NonNull; - -import java.util.concurrent.Executor; - -/** - * A {@link TaskCompletionListener} that wraps a {@link Continuation}. - */ -class ContinueWithCompletionListener implements TaskCompletionListener { - - private final Executor executor; - private final Continuation continuation; - private final TaskImpl continuationTask; - - public ContinueWithCompletionListener( - @NonNull Executor executor, - @NonNull Continuation continuation, - @NonNull TaskImpl continuationTask) { - this.executor = executor; - this.continuation = continuation; - this.continuationTask = continuationTask; - } - - @Override - public void onComplete(@NonNull final Task task) { - executor.execute( - new Runnable() { - @Override - public void run() { - R result; - try { - result = continuation.then(task); - } catch (RuntimeExecutionException e) { - if (e.getCause() instanceof Exception) { - continuationTask.setException((Exception) e.getCause()); - } else { - continuationTask.setException(e); - } - return; - } catch (Exception e) { - continuationTask.setException(e); - return; - } - - continuationTask.setResult(result); - } - }); - } - - @Override - public void cancel() { - throw new UnsupportedOperationException(); - } -} diff --git a/src/main/java/com/google/firebase/tasks/ContinueWithTaskCompletionListener.java b/src/main/java/com/google/firebase/tasks/ContinueWithTaskCompletionListener.java deleted file mode 100644 index ffbf88f90..000000000 --- a/src/main/java/com/google/firebase/tasks/ContinueWithTaskCompletionListener.java +++ /dev/null @@ -1,90 +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.tasks; - -import com.google.firebase.internal.NonNull; - -import java.util.concurrent.Executor; - -/** - * A {@link TaskCompletionListener} that wraps a {@link Continuation} that returns a {@link Task}. - */ -class ContinueWithTaskCompletionListener - implements TaskCompletionListener, OnSuccessListener, OnFailureListener { - - private final Executor executor; - private final Continuation> continuation; - private final TaskImpl continuationTask; - - public ContinueWithTaskCompletionListener( - @NonNull Executor executor, - @NonNull Continuation> continuation, - @NonNull TaskImpl continuationTask) { - this.executor = executor; - this.continuation = continuation; - this.continuationTask = continuationTask; - } - - @Override - public void onComplete(@NonNull final Task task) { - executor.execute( - new Runnable() { - @Override - public void run() { - Task resultTask; - try { - resultTask = continuation.then(task); - } catch (RuntimeExecutionException e) { - if (e.getCause() instanceof Exception) { - continuationTask.setException((Exception) e.getCause()); - } else { - continuationTask.setException(e); - } - return; - } catch (Exception e) { - continuationTask.setException(e); - return; - } - - if (resultTask == null) { - onFailure(new NullPointerException("Continuation returned null")); - return; - } - - resultTask.addOnSuccessListener( - TaskExecutors.DIRECT, ContinueWithTaskCompletionListener.this); - resultTask.addOnFailureListener( - TaskExecutors.DIRECT, ContinueWithTaskCompletionListener.this); - } - }); - } - - @Override - public void onSuccess(R result) { - continuationTask.setResult(result); - } - - @Override - public void onFailure(@NonNull Exception e) { - continuationTask.setException(e); - } - - @Override - public void cancel() { - throw new UnsupportedOperationException(); - } -} diff --git a/src/main/java/com/google/firebase/tasks/OnCompleteCompletionListener.java b/src/main/java/com/google/firebase/tasks/OnCompleteCompletionListener.java deleted file mode 100644 index e492fa596..000000000 --- a/src/main/java/com/google/firebase/tasks/OnCompleteCompletionListener.java +++ /dev/null @@ -1,67 +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.tasks; - -import com.google.firebase.internal.GuardedBy; -import com.google.firebase.internal.NonNull; - -import java.util.concurrent.Executor; - -/** - * A {@link TaskCompletionListener} that wraps an {@link OnCompleteListener}. - */ -class OnCompleteCompletionListener implements TaskCompletionListener { - - private final Executor executor; - private final Object lock = new Object(); - - @GuardedBy("lock") - private OnCompleteListener onComplete; - - public OnCompleteCompletionListener( - @NonNull Executor executor, @NonNull OnCompleteListener onComplete) { - this.executor = executor; - this.onComplete = onComplete; - } - - @Override - public void onComplete(@NonNull final Task task) { - synchronized (lock) { - if (onComplete == null) { - return; - } - } - executor.execute( - new Runnable() { - @Override - public void run() { - synchronized (lock) { - if (onComplete != null) { - onComplete.onComplete(task); - } - } - } - }); - } - - @Override - public void cancel() { - synchronized (lock) { - onComplete = null; - } - } -} diff --git a/src/main/java/com/google/firebase/tasks/OnCompleteListener.java b/src/main/java/com/google/firebase/tasks/OnCompleteListener.java deleted file mode 100644 index b92fb8eb0..000000000 --- a/src/main/java/com/google/firebase/tasks/OnCompleteListener.java +++ /dev/null @@ -1,35 +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.tasks; - -import com.google.firebase.internal.NonNull; - -/** - * Listener called when a {@link Task} completes. - * - * @param the Task's result type - * @see Task#addOnCompleteListener(OnCompleteListener) - */ -public interface OnCompleteListener { - - /** - * Called when the Task completes. - * - * @param task the completed Task. Never null - */ - void onComplete(@NonNull Task task); -} diff --git a/src/main/java/com/google/firebase/tasks/OnFailureCompletionListener.java b/src/main/java/com/google/firebase/tasks/OnFailureCompletionListener.java deleted file mode 100644 index 114d9eb62..000000000 --- a/src/main/java/com/google/firebase/tasks/OnFailureCompletionListener.java +++ /dev/null @@ -1,69 +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.tasks; - -import com.google.firebase.internal.GuardedBy; -import com.google.firebase.internal.NonNull; - -import java.util.concurrent.Executor; - -/** - * A {@link TaskCompletionListener} that wraps an {@link OnFailureListener}. - */ -class OnFailureCompletionListener implements TaskCompletionListener { - - private final Executor executor; - private final Object lock = new Object(); - - @GuardedBy("lock") - private OnFailureListener onFailure; - - public OnFailureCompletionListener( - @NonNull Executor executor, @NonNull OnFailureListener onFailure) { - this.executor = executor; - this.onFailure = onFailure; - } - - @Override - public void onComplete(@NonNull final Task task) { - if (!task.isSuccessful()) { - synchronized (lock) { - if (onFailure == null) { - return; - } - } - executor.execute( - new Runnable() { - @Override - public void run() { - synchronized (lock) { - if (onFailure != null) { - onFailure.onFailure(task.getException()); - } - } - } - }); - } - } - - @Override - public void cancel() { - synchronized (lock) { - onFailure = null; - } - } -} diff --git a/src/main/java/com/google/firebase/tasks/OnFailureListener.java b/src/main/java/com/google/firebase/tasks/OnFailureListener.java deleted file mode 100644 index be44b9837..000000000 --- a/src/main/java/com/google/firebase/tasks/OnFailureListener.java +++ /dev/null @@ -1,34 +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.tasks; - -import com.google.firebase.internal.NonNull; - -/** - * Listener called when a {@link Task} fails with an exception. - * - * @see Task#addOnFailureListener(OnFailureListener) - */ -public interface OnFailureListener { - - /** - * Called when the Task fails with an exception. - * - * @param e the exception that caused the Task to fail. Never null - */ - void onFailure(@NonNull Exception e); -} diff --git a/src/main/java/com/google/firebase/tasks/OnSuccessCompletionListener.java b/src/main/java/com/google/firebase/tasks/OnSuccessCompletionListener.java deleted file mode 100644 index 23a863c03..000000000 --- a/src/main/java/com/google/firebase/tasks/OnSuccessCompletionListener.java +++ /dev/null @@ -1,69 +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.tasks; - -import com.google.firebase.internal.GuardedBy; -import com.google.firebase.internal.NonNull; - -import java.util.concurrent.Executor; - -/** - * A {@link TaskCompletionListener} that wraps an {@link OnSuccessListener}. - */ -class OnSuccessCompletionListener implements TaskCompletionListener { - - private final Executor executor; - private final Object lock = new Object(); - - @GuardedBy("lock") - private OnSuccessListener onSuccess; - - public OnSuccessCompletionListener( - @NonNull Executor executor, @NonNull OnSuccessListener onSuccess) { - this.executor = executor; - this.onSuccess = onSuccess; - } - - @Override - public void onComplete(@NonNull final Task task) { - if (task.isSuccessful()) { - synchronized (lock) { - if (onSuccess == null) { - return; - } - } - executor.execute( - new Runnable() { - @Override - public void run() { - synchronized (lock) { - if (onSuccess != null) { - onSuccess.onSuccess(task.getResult()); - } - } - } - }); - } - } - - @Override - public void cancel() { - synchronized (lock) { - onSuccess = null; - } - } -} diff --git a/src/main/java/com/google/firebase/tasks/OnSuccessListener.java b/src/main/java/com/google/firebase/tasks/OnSuccessListener.java deleted file mode 100644 index 3e84d5c10..000000000 --- a/src/main/java/com/google/firebase/tasks/OnSuccessListener.java +++ /dev/null @@ -1,33 +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.tasks; - -/** - * Listener called when a {@link Task} completes successfully. - * - * @param the Task's result type - * @see Task#addOnSuccessListener(OnSuccessListener) - */ -public interface OnSuccessListener { - - /** - * Called when the {@link Task} completes successfully. - * - * @param result the result of the Task - */ - void onSuccess(T result); -} diff --git a/src/main/java/com/google/firebase/tasks/Task.java b/src/main/java/com/google/firebase/tasks/Task.java deleted file mode 100644 index 13f3e85c0..000000000 --- a/src/main/java/com/google/firebase/tasks/Task.java +++ /dev/null @@ -1,214 +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.tasks; - -import com.google.firebase.internal.NonNull; -import com.google.firebase.internal.Nullable; - -import java.util.concurrent.Executor; - -/** - * Represents an asynchronous operation. - * - * @param the type of the result of the operation - * @deprecated {@code Task} has been deprecated in favor of - *
    {@code ApiFuture}. - * For every method x() that returns a {@code Task}, you should be able to find a - * corresponding xAsync() method that returns an {@code ApiFuture}. - */ -public abstract class Task { - - /** - * Returns {@code true} if the Task is complete; {@code false} otherwise. - */ - public abstract boolean isComplete(); - - /** - * Returns {@code true} if the Task has completed successfully; {@code false} otherwise. - */ - public abstract boolean isSuccessful(); - - /** - * Gets the result of the Task, if it has already completed. - * - * @throws IllegalStateException if the Task is not yet complete - * @throws RuntimeExecutionException if the Task failed with an exception - */ - public abstract T getResult(); - - /** - * Gets the result of the Task, if it has already completed. - * - * @throws IllegalStateException if the Task is not yet complete - * @throws X if the Task failed with an exception of type X - * @throws RuntimeExecutionException if the Task failed with an exception that was not of type X - */ - public abstract T getResult(@NonNull Class exceptionType) throws X; - - /** - * Returns the exception that caused the Task to fail. Returns {@code null} if the Task is not yet - * complete, or completed successfully. - */ - @Nullable - public abstract Exception getException(); - - /** - * Adds a listener that is called if the Task completes successfully. - * - *

    The listener will be called on a shared thread pool. If the Task has already completed - * successfully, a call to the listener will be immediately scheduled. If multiple listeners are - * added, they will be called in the order in which they were added. - * - * @return this Task - * - * @deprecated Use {@link #addOnSuccessListener(Executor, OnSuccessListener)} - */ - @NonNull - public abstract Task addOnSuccessListener(@NonNull OnSuccessListener listener); - - /** - * Adds a listener that is called if the Task completes successfully. - * - *

    If multiple listeners are added, they will be called in the order in which they were added. - * If the Task has already completed successfully, a call to the listener will be immediately - * scheduled. - * - * @param executor the executor to use to call the listener - * @return this Task - */ - @NonNull - public abstract Task addOnSuccessListener( - @NonNull Executor executor, @NonNull OnSuccessListener listener); - - /** - * Adds a listener that is called if the Task fails. - * - *

    The listener will be called on a shared thread pool. If the Task has already failed, a call - * to the listener will be immediately scheduled. If multiple listeners are added, they will be - * called in the order in which they were added. - * - * @return this Task - * - * @deprecated Use {@link #addOnFailureListener(Executor, OnFailureListener)} - */ - @NonNull - public abstract Task addOnFailureListener(@NonNull OnFailureListener listener); - - /** - * Adds a listener that is called if the Task fails. - * - *

    If the Task has already failed, a call to the listener will be immediately scheduled. If - * multiple listeners are added, they will be called in the order in which they were added. - * - * @param executor the executor to use to call the listener - * @return this Task - */ - @NonNull - public abstract Task addOnFailureListener( - @NonNull Executor executor, @NonNull OnFailureListener listener); - - /** - * Adds a listener that is called when the Task completes. - * - *

    The listener will be called on a shared thread pool. If the Task is already complete, a call - * to the listener will be immediately scheduled. If multiple listeners are added, they will be - * called in the order in which they were added. - * - * @return this Task - * - * @deprecated Use {@link #addOnCompleteListener(Executor, OnCompleteListener)} - */ - @NonNull - public Task addOnCompleteListener(@NonNull OnCompleteListener listener) { - throw new UnsupportedOperationException("addOnCompleteListener is not implemented"); - } - - /** - * Adds a listener that is called when the Task completes. - * - *

    If the Task is already complete, a call to the listener will be immediately scheduled. If - * multiple listeners are added, they will be called in the order in which they were added. - * - * @param executor the executor to use to call the listener - * @return this Task - */ - @NonNull - public Task addOnCompleteListener( - @NonNull Executor executor, @NonNull OnCompleteListener listener) { - throw new UnsupportedOperationException("addOnCompleteListener is not implemented"); - } - - /** - * Returns a new Task that will be completed with the result of applying the specified - * Continuation to this Task. - * - *

    If the Continuation throws an exception, the returned Task will fail with that exception. - * - *

    The Continuation will be called on a shared thread pool. - * - * @deprecated Use {@link #continueWith(Executor, Continuation)}. - */ - @NonNull - public Task continueWith(@NonNull Continuation continuation) { - throw new UnsupportedOperationException("continueWith is not implemented"); - } - - /** - * Returns a new Task that will be completed with the result of applying the specified - * Continuation to this Task. - * - *

    If the Continuation throws an exception, the returned Task will fail with that exception. - * - * @param executor the executor to use to call the Continuation - * @see Continuation#then(Task) - */ - @NonNull - public Task continueWith( - @NonNull Executor executor, @NonNull Continuation continuation) { - throw new UnsupportedOperationException("continueWith is not implemented"); - } - - /** - * Returns a new Task that will be completed with the result of applying the specified - * Continuation to this Task. - * - *

    If the Continuation throws an exception, the returned Task will fail with that exception. - * - *

    The Continuation will be called on a shared thread pool. - * - * @deprecated Use {@link #continueWithTask(Executor, Continuation)} - */ - @NonNull - public Task continueWithTask(@NonNull Continuation> continuation) { - throw new UnsupportedOperationException("continueWithTask is not implemented"); - } - - /** - * Returns a new Task that will be completed with the result of applying the specified - * Continuation to this Task. - * - *

    If the Continuation throws an exception, the returned Task will fail with that exception. - * - * @param executor the executor to use to call the Continuation - * @see Continuation#then(Task) - */ - @NonNull - public Task continueWithTask( - @NonNull Executor executor, @NonNull Continuation> continuation) { - throw new UnsupportedOperationException("continueWithTask is not implemented"); - } -} diff --git a/src/main/java/com/google/firebase/tasks/TaskCompletionListener.java b/src/main/java/com/google/firebase/tasks/TaskCompletionListener.java deleted file mode 100644 index 2e261cab9..000000000 --- a/src/main/java/com/google/firebase/tasks/TaskCompletionListener.java +++ /dev/null @@ -1,31 +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.tasks; - -import com.google.firebase.internal.NonNull; - -/** - * A listener that is called upon {@link Task} completion. - * - * @param Task result type. - */ -interface TaskCompletionListener { - - void onComplete(@NonNull Task task); - - void cancel(); -} diff --git a/src/main/java/com/google/firebase/tasks/TaskCompletionListenerQueue.java b/src/main/java/com/google/firebase/tasks/TaskCompletionListenerQueue.java deleted file mode 100644 index ef59e19fd..000000000 --- a/src/main/java/com/google/firebase/tasks/TaskCompletionListenerQueue.java +++ /dev/null @@ -1,84 +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.tasks; - -import com.google.firebase.internal.GuardedBy; -import com.google.firebase.internal.NonNull; - -import java.util.ArrayDeque; -import java.util.Collection; -import java.util.Queue; - -/** - * A queue of listeners to call upon {@link Task} completion. - * - * @param Task result type. - */ -class TaskCompletionListenerQueue { - - private final Object lock = new Object(); - - /** Lazily initialized, unbounded queue of listeners to call. */ - @GuardedBy("lock") - private Queue> queue; - - /** - * Indicates if a flush is already in progress. While this is true, further calls to flush() will - * do nothing. - */ - @GuardedBy("lock") - private boolean flushing; - - // TODO: Define behaviour for duplicate listeners. - public void add(@NonNull TaskCompletionListener listener) { - synchronized (lock) { - if (queue == null) { - queue = new ArrayDeque<>(); - } - queue.add(listener); - } - } - - public boolean removeAll(@NonNull Collection> listeners) { - synchronized (lock) { - return queue == null || queue.removeAll(listeners); - } - } - - public void flush(@NonNull Task task) { - synchronized (lock) { - if (queue == null || flushing) { - return; - } - flushing = true; - } - - while (true) { - TaskCompletionListener next; - synchronized (lock) { - next = queue.poll(); - if (next == null) { - flushing = false; - return; - } - } - - // Call outside the lock to avoid potential deadlocks with client code. - next.onComplete(task); - } - } -} diff --git a/src/main/java/com/google/firebase/tasks/TaskCompletionSource.java b/src/main/java/com/google/firebase/tasks/TaskCompletionSource.java deleted file mode 100644 index 12db74843..000000000 --- a/src/main/java/com/google/firebase/tasks/TaskCompletionSource.java +++ /dev/null @@ -1,72 +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.tasks; - -import com.google.firebase.internal.NonNull; - -/** - * Provides the ability to create an incomplete {@link Task} and later complete it by either calling - * {@link #setResult} or {@link #setException}. - */ -public class TaskCompletionSource { - - private final TaskImpl task = new TaskImpl<>(); - - /** - * Completes the Task with the specified result. - * - * @throws IllegalStateException if the Task is already complete - */ - public void setResult(T result) { - task.setResult(result); - } - - /** - * Completes the Task with the specified result, unless the Task has already completed. If the - * Task has already completed, the call does nothing. - * - * @return {@code true} if the result was set successfully, {@code false} otherwise - */ - public boolean trySetResult(T result) { - return task.trySetResult(result); - } - - /** - * Completes the Task with the specified exception. - * - * @throws IllegalStateException if the Task is already complete - */ - public void setException(@NonNull Exception e) { - task.setException(e); - } - - /** - * Completes the Task with the specified exception, unless the Task has already completed. If the - * Task has already completed, the call does nothing. - * - * @return {@code true} if the exception was set successfully, {@code false} otherwise - */ - public boolean trySetException(@NonNull Exception e) { - return task.trySetException(e); - } - - /** Returns the Task. */ - @NonNull - public Task getTask() { - return task; - } -} diff --git a/src/main/java/com/google/firebase/tasks/TaskExecutors.java b/src/main/java/com/google/firebase/tasks/TaskExecutors.java deleted file mode 100644 index 19674d1d1..000000000 --- a/src/main/java/com/google/firebase/tasks/TaskExecutors.java +++ /dev/null @@ -1,64 +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.tasks; - -import com.google.common.util.concurrent.ThreadFactoryBuilder; -import com.google.firebase.internal.GaeThreadFactory; -import com.google.firebase.internal.NonNull; - -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; - -/** - * Standard {@link Executor} instances for use with {@link Task}. - * - * @deprecated Use the ThreadManager interface to get required Executors. - */ -public class TaskExecutors { - - /** - * An Executor that uses a shared cached thread pool. - * - *

    This is no longer used in the SDK code. All the methods that submit to this thread pool - * have been deprecated, and their invocations have been routed elsewhere. This is left here - * for now for backward compatibility, since technically it is part of the public API. - */ - public static final Executor DEFAULT_THREAD_POOL; - /** An Executor that uses the calling thread. */ - static final Executor DIRECT = - new Executor() { - @Override - public void execute(@NonNull Runnable command) { - command.run(); - } - }; - - static { - if (GaeThreadFactory.isAvailable()) { - DEFAULT_THREAD_POOL = GaeThreadFactory.DEFAULT_EXECUTOR; - } else { - ThreadFactory threadFactory = new ThreadFactoryBuilder() - .setNameFormat("task-exec-%d") - .setDaemon(true) - .build(); - DEFAULT_THREAD_POOL = Executors.newCachedThreadPool(threadFactory); - } - } - - private TaskExecutors() {} -} diff --git a/src/main/java/com/google/firebase/tasks/TaskImpl.java b/src/main/java/com/google/firebase/tasks/TaskImpl.java deleted file mode 100644 index 095909e40..000000000 --- a/src/main/java/com/google/firebase/tasks/TaskImpl.java +++ /dev/null @@ -1,244 +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.tasks; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.base.Preconditions; -import com.google.firebase.internal.GuardedBy; -import com.google.firebase.internal.NonNull; -import com.google.firebase.internal.Nullable; - -import java.util.concurrent.Executor; - -/** - * Default implementation of {@link Task}. - */ -final class TaskImpl extends Task { - - private final Object lock = new Object(); - private final TaskCompletionListenerQueue listenerQueue = new TaskCompletionListenerQueue<>(); - - @GuardedBy("lock") - private boolean complete; - - @GuardedBy("lock") - private T result; - - @GuardedBy("lock") - private Exception exception; - - @Override - public boolean isComplete() { - synchronized (lock) { - return complete; - } - } - - @Override - public boolean isSuccessful() { - synchronized (lock) { - return complete && exception == null; - } - } - - @Override - public T getResult() { - synchronized (lock) { - checkCompleteLocked(); - - if (exception != null) { - throw new RuntimeExecutionException(exception); - } - - return result; - } - } - - @Override - public T getResult(@NonNull Class exceptionType) throws X { - synchronized (lock) { - checkCompleteLocked(); - - if (exceptionType.isInstance(exception)) { - throw exceptionType.cast(exception); - } - if (exception != null) { - throw new RuntimeExecutionException(exception); - } - - return result; - } - } - - public void setResult(T result) { - synchronized (lock) { - checkNotCompleteLocked(); - complete = true; - this.result = result; - } - // Intentionally outside the lock. - listenerQueue.flush(this); - } - - @Nullable - @Override - public Exception getException() { - synchronized (lock) { - return exception; - } - } - - @SuppressWarnings("ThrowableResultOfMethodCallIgnored") - public void setException(@NonNull Exception e) { - checkNotNull(e, "Exception must not be null"); - synchronized (lock) { - checkNotCompleteLocked(); - complete = true; - exception = e; - } - // Intentionally outside the lock. - listenerQueue.flush(this); - } - - @NonNull - @Override - public Task addOnSuccessListener(@NonNull OnSuccessListener listener) { - return addOnSuccessListener(TaskExecutors.DEFAULT_THREAD_POOL, listener); - } - - @NonNull - @Override - public Task addOnSuccessListener( - @NonNull Executor executor, @NonNull OnSuccessListener listener) { - listenerQueue.add(new OnSuccessCompletionListener<>(executor, listener)); - flushIfComplete(); - return this; - } - - @NonNull - @Override - public Task addOnFailureListener(@NonNull OnFailureListener listener) { - return addOnFailureListener(TaskExecutors.DEFAULT_THREAD_POOL, listener); - } - - @NonNull - @Override - public Task addOnFailureListener( - @NonNull Executor executor, @NonNull OnFailureListener listener) { - listenerQueue.add(new OnFailureCompletionListener(executor, listener)); - flushIfComplete(); - return this; - } - - @NonNull - @Override - public Task addOnCompleteListener(@NonNull OnCompleteListener listener) { - return addOnCompleteListener(TaskExecutors.DEFAULT_THREAD_POOL, listener); - } - - @NonNull - @Override - public Task addOnCompleteListener( - @NonNull Executor executor, @NonNull OnCompleteListener listener) { - listenerQueue.add(new OnCompleteCompletionListener<>(executor, listener)); - flushIfComplete(); - return this; - } - - @NonNull - @Override - public Task continueWith(@NonNull Continuation continuation) { - return continueWith(TaskExecutors.DEFAULT_THREAD_POOL, continuation); - } - - @NonNull - @Override - public Task continueWith( - @NonNull Executor executor, @NonNull Continuation continuation) { - TaskImpl continuationTask = new TaskImpl<>(); - listenerQueue.add( - new ContinueWithCompletionListener<>(executor, continuation, continuationTask)); - flushIfComplete(); - return continuationTask; - } - - @NonNull - @Override - public Task continueWithTask(@NonNull Continuation> continuation) { - return continueWithTask(TaskExecutors.DEFAULT_THREAD_POOL, continuation); - } - - @NonNull - @Override - public Task continueWithTask( - @NonNull Executor executor, @NonNull Continuation> continuation) { - TaskImpl continuationTask = new TaskImpl<>(); - listenerQueue.add( - new ContinueWithTaskCompletionListener<>(executor, continuation, continuationTask)); - flushIfComplete(); - return continuationTask; - } - - public boolean trySetResult(T result) { - synchronized (lock) { - if (complete) { - return false; - } - complete = true; - this.result = result; - } - // Intentionally outside the lock. - listenerQueue.flush(this); - return true; - } - - @SuppressWarnings("ThrowableResultOfMethodCallIgnored") - public boolean trySetException(@NonNull Exception e) { - checkNotNull(e, "Exception must not be null"); - synchronized (lock) { - if (complete) { - return false; - } - complete = true; - exception = e; - } - // Intentionally outside the lock. - listenerQueue.flush(this); - return true; - } - - @GuardedBy("lock") - private void checkCompleteLocked() { - Preconditions.checkState(complete, "Task is not yet complete"); - } - - @GuardedBy("lock") - private void checkNotCompleteLocked() { - Preconditions.checkState(!complete, "Task is already complete"); - } - - private void flushIfComplete() { - synchronized (lock) { - if (!complete) { - return; - } - } - // Intentionally outside the lock. - listenerQueue.flush(this); - } -} diff --git a/src/main/java/com/google/firebase/tasks/Tasks.java b/src/main/java/com/google/firebase/tasks/Tasks.java deleted file mode 100644 index 3671ac14f..000000000 --- a/src/main/java/com/google/firebase/tasks/Tasks.java +++ /dev/null @@ -1,264 +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.tasks; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.firebase.internal.GuardedBy; -import com.google.firebase.internal.NonNull; - -import java.util.Arrays; -import java.util.Collection; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -/** - * {@link Task} utility methods. - * - * @deprecated Use ApiFutures and ThreadManager interface instead. - */ -public final class Tasks { - - private Tasks() {} - - /** Returns a completed Task with the specified result. */ - public static Task forResult(T result) { - TaskImpl task = new TaskImpl<>(); - task.setResult(result); - return task; - } - - /** Returns a completed Task with the specified exception. */ - public static Task forException(@NonNull Exception exception) { - TaskImpl task = new TaskImpl<>(); - task.setException(exception); - return task; - } - - /** - * Returns a Task that will be completed with the result of the specified Callable. - * - *

    The Callable will be called on a shared thread pool. - * - * @deprecated Use {@link #call(Executor, Callable)} - */ - public static Task call(@NonNull Callable callable) { - return call(TaskExecutors.DEFAULT_THREAD_POOL, callable); - } - - /** - * Returns a Task that will be completed with the result of the specified Callable. - * - * @param executor the Executor to use to call the Callable - */ - public static Task call(@NonNull Executor executor, @NonNull final Callable callable) { - checkNotNull(executor, "Executor must not be null"); - checkNotNull(callable, "Callback must not be null"); - - final TaskImpl task = new TaskImpl<>(); - executor.execute( - new Runnable() { - @Override - public void run() { - try { - task.setResult(callable.call()); - } catch (Exception e) { - task.setException(e); - } - } - }); - return task; - } - - /** - * Blocks until the specified Task is complete. - * - * @return the Task's result - * @throws ExecutionException if the Task fails - * @throws InterruptedException if an interrupt occurs while waiting for the Task to complete - */ - public static T await(@NonNull Task task) throws ExecutionException, InterruptedException { - checkNotNull(task, "Task must not be null"); - - if (task.isComplete()) { - return getResultOrThrowExecutionException(task); - } - - AwaitListener listener = new AwaitListener(); - addListener(task, listener); - listener.await(); - - return getResultOrThrowExecutionException(task); - } - - /** - * Blocks until the specified Task is complete. - * - * @return the Task's result - * @throws ExecutionException if the Task fails - * @throws InterruptedException if an interrupt occurs while waiting for the Task to complete - * @throws TimeoutException if the specified timeout is reached before the Task completes - */ - public static T await(@NonNull Task task, long timeout, @NonNull TimeUnit unit) - throws ExecutionException, InterruptedException, TimeoutException { - checkNotNull(task, "Task must not be null"); - checkNotNull(unit, "TimeUnit must not be null"); - - if (task.isComplete()) { - return getResultOrThrowExecutionException(task); - } - - AwaitListener listener = new AwaitListener(); - addListener(task, listener); - - if (!listener.await(timeout, unit)) { - throw new TimeoutException("Timed out waiting for Task"); - } - - return getResultOrThrowExecutionException(task); - } - - /** - * Returns a Task that completes successfully when all of the specified Tasks complete - * successfully. Does not accept nulls. - * - * @throws NullPointerException if any of the provided Tasks are null - */ - public static Task whenAll(final Collection> tasks) { - if (tasks.isEmpty()) { - return Tasks.forResult(null); - } - for (Task task : tasks) { - if (task == null) { - throw new NullPointerException("null tasks are not accepted"); - } - } - TaskImpl whenAllTask = new TaskImpl<>(); - WhenAllListener listener = new WhenAllListener(tasks.size(), whenAllTask); - for (Task task : tasks) { - addListener(task, listener); - } - return whenAllTask; - } - - /** - * Returns a Task that completes successfully when all of the specified Tasks complete - * successfully. Does not accept nulls. - * - * @throws NullPointerException if any of the provided Tasks are null - */ - public static Task whenAll(Task... tasks) { - if (tasks.length == 0) { - return Tasks.forResult(null); - } - return whenAll(Arrays.asList(tasks)); - } - - private static T getResultOrThrowExecutionException(Task task) throws ExecutionException { - if (task.isSuccessful()) { - return task.getResult(); - } else { - throw new ExecutionException(task.getException()); - } - } - - private static void addListener(Task task, CombinedListener listener) { - // Use a direct executor to avoid an additional thread-hop. - task.addOnSuccessListener(TaskExecutors.DIRECT, listener); - task.addOnFailureListener(TaskExecutors.DIRECT, listener); - } - - interface CombinedListener extends OnSuccessListener, OnFailureListener {} - - private static final class AwaitListener implements CombinedListener { - - private final CountDownLatch latch = new CountDownLatch(1); - - @Override - public void onSuccess(Object obj) { - latch.countDown(); - } - - @Override - public void onFailure(@NonNull Exception exception) { - latch.countDown(); - } - - public void await() throws InterruptedException { - latch.await(); - } - - public boolean await(long timeout, TimeUnit unit) throws InterruptedException { - return latch.await(timeout, unit); - } - } - - private static final class WhenAllListener implements CombinedListener { - - private final Object lock = new Object(); - private final int numTasks; - private final TaskImpl task; - - @GuardedBy("lock") - private int successCounter; - - @GuardedBy("lock") - private int failuresCounter; - - @GuardedBy("lock") - private Exception exception; - - public WhenAllListener(int taskCount, TaskImpl task) { - numTasks = taskCount; - this.task = task; - } - - @Override - public void onFailure(@NonNull Exception exception) { - synchronized (lock) { - failuresCounter++; - this.exception = exception; - checkForCompletionLocked(); - } - } - - @Override - public void onSuccess(Object obj) { - synchronized (lock) { - successCounter++; - checkForCompletionLocked(); - } - } - - @GuardedBy("lock") - private void checkForCompletionLocked() { - if (successCounter + failuresCounter == numTasks) { - if (exception == null) { - task.setResult(null); - } else { - task.setException( - new ExecutionException( - failuresCounter + " out of " + numTasks + " underlying tasks failed", exception)); - } - } - } - } -} diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java index d90679d05..e549c0955 100644 --- a/src/test/java/com/google/firebase/FirebaseAppTest.java +++ b/src/test/java/com/google/firebase/FirebaseAppTest.java @@ -16,7 +16,6 @@ package com.google.firebase; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; @@ -35,7 +34,6 @@ import com.google.auth.oauth2.OAuth2Credentials.CredentialsChangedListener; import com.google.common.base.Defaults; import com.google.common.collect.ImmutableMap; -import com.google.common.io.BaseEncoding; import com.google.firebase.FirebaseApp.TokenRefresher; import com.google.firebase.FirebaseOptions.Builder; import com.google.firebase.database.FirebaseDatabase; @@ -233,15 +231,6 @@ public void testInvokeAfterDeleteThrows() throws Exception { } } - @Test - public void testPersistenceKey() { - String name = "myApp"; - FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS, name); - String persistenceKey = firebaseApp.getPersistenceKey(); - assertEquals(name, new String(BaseEncoding.base64Url().omitPadding().decode(persistenceKey), - UTF_8)); - } - // Order of test cases matters. @Test(expected = IllegalStateException.class) public void testMissingInit() { @@ -441,19 +430,19 @@ public void testEmptyFirebaseConfigJSONObject() { assertTrue(firebaseApp.getOptions().getDatabaseAuthVariableOverride().isEmpty()); } - @Test(expected = IllegalStateException.class) + @Test(expected = IllegalArgumentException.class) public void testInvalidFirebaseConfigFile() { setFirebaseConfigEnvironmentVariable("firebase_config_invalid.json"); FirebaseApp.initializeApp(); } - @Test(expected = IllegalStateException.class) + @Test(expected = IllegalArgumentException.class) public void testInvalidFirebaseConfigString() { setFirebaseConfigEnvironmentVariable("{,,"); FirebaseApp.initializeApp(); } - @Test(expected = IllegalStateException.class) + @Test(expected = IllegalArgumentException.class) public void testFirebaseConfigMissingFile() { setFirebaseConfigEnvironmentVariable("no_such.json"); FirebaseApp.initializeApp(); diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java index 33e07aeb5..72a9c4784 100644 --- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java +++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java @@ -27,12 +27,9 @@ import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.gson.GsonFactory; +import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.ServiceAccountCredentials; -import com.google.firebase.auth.FirebaseCredential; -import com.google.firebase.auth.FirebaseCredentials; -import com.google.firebase.auth.GoogleOAuthAccessToken; -import com.google.firebase.tasks.Task; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; import java.io.IOException; @@ -75,7 +72,7 @@ protected ThreadFactory getThreadFactory() { }; @Test - public void createOptionsWithAllValuesSet() throws IOException, InterruptedException { + public void createOptionsWithAllValuesSet() throws IOException { GsonFactory jsonFactory = new GsonFactory(); NetHttpTransport httpTransport = new NetHttpTransport(); FirebaseOptions firebaseOptions = @@ -108,7 +105,7 @@ public void createOptionsWithAllValuesSet() throws IOException, InterruptedExcep } @Test - public void createOptionsWithOnlyMandatoryValuesSet() throws IOException, InterruptedException { + public void createOptionsWithOnlyMandatoryValuesSet() throws IOException { FirebaseOptions firebaseOptions = new FirebaseOptions.Builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) @@ -130,32 +127,12 @@ public void createOptionsWithOnlyMandatoryValuesSet() throws IOException, Interr } @Test - public void createOptionsWithFirebaseCredential() throws IOException { + public void createOptionsWithCustomFirebaseCredential() { FirebaseOptions firebaseOptions = new FirebaseOptions.Builder() - .setCredential(FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream())) - .build(); - - assertNotNull(firebaseOptions.getJsonFactory()); - assertNotNull(firebaseOptions.getHttpTransport()); - assertNull(firebaseOptions.getDatabaseUrl()); - assertNull(firebaseOptions.getStorageBucket()); - - GoogleCredentials credentials = firebaseOptions.getCredentials(); - assertNotNull(credentials); - assertTrue(credentials instanceof ServiceAccountCredentials); - assertEquals( - GoogleCredential.fromStream(ServiceAccount.EDITOR.asStream()).getServiceAccountId(), - ((ServiceAccountCredentials) credentials).getClientEmail()); - } - - @Test - public void createOptionsWithCustomFirebaseCredential() throws IOException { - FirebaseOptions firebaseOptions = - new FirebaseOptions.Builder() - .setCredential(new FirebaseCredential() { + .setCredentials(new GoogleCredentials() { @Override - public Task getAccessToken() { + public AccessToken refreshAccessToken() { return null; } }) diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index e2c709ccd..bb430d285 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -106,16 +106,6 @@ public static Collection data() throws Exception { .build(), /* isCertCredential */ false }, - { - new FirebaseOptions.Builder().setCredential( - createFirebaseCertificateCredential()).build(), - /* isCertCredential */ true - }, - { - new FirebaseOptions.Builder().setCredential( - createFirebaseRefreshTokenCredential()).build(), - /* isCertCredential */ false - }, }); } @@ -141,23 +131,6 @@ public HttpTransport create() { }); } - private static FirebaseCredential createFirebaseRefreshTokenCredential() - 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 FirebaseCredentials.fromRefreshToken(refreshTokenStream, transport, JSON_FACTORY); - } - private static GoogleCredentials createCertificateCredential() throws IOException { final MockTokenServerTransport transport = new MockTokenServerTransport(); transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), ACCESS_TOKEN); @@ -170,13 +143,6 @@ public HttpTransport create() { }); } - private static FirebaseCredential createFirebaseCertificateCredential() throws IOException { - final MockTokenServerTransport transport = new MockTokenServerTransport(); - transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), ACCESS_TOKEN); - return FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream(), - transport, JSON_FACTORY); - } - @Before public void setup() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -189,7 +155,7 @@ public void cleanup() { } @Test - public void testGetInstance() throws ExecutionException, InterruptedException { + public void testGetInstance() { FirebaseAuth defaultAuth = FirebaseAuth.getInstance(); assertNotNull(defaultAuth); assertSame(defaultAuth, FirebaseAuth.getInstance()); @@ -198,7 +164,7 @@ public void testGetInstance() throws ExecutionException, InterruptedException { } @Test - public void testGetInstanceForApp() throws ExecutionException, InterruptedException { + public void testGetInstanceForApp() { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testGetInstanceForApp"); FirebaseAuth auth = FirebaseAuth.getInstance(app); assertNotNull(auth); @@ -208,7 +174,7 @@ public void testGetInstanceForApp() throws ExecutionException, InterruptedExcept } @Test - public void testAppDelete() throws ExecutionException, InterruptedException { + public void testAppDelete() { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testAppDelete"); FirebaseAuth auth = FirebaseAuth.getInstance(app); assertNotNull(auth); @@ -272,7 +238,7 @@ public void testInitAfterAppDelete() throws ExecutionException, InterruptedExcep } @Test - public void testAppWithAuthVariableOverrides() throws ExecutionException, InterruptedException { + public void testAppWithAuthVariableOverrides() { Map authVariableOverrides = Collections.singletonMap("uid", (Object) "uid1"); FirebaseOptions options = new FirebaseOptions.Builder(firebaseOptions) @@ -335,7 +301,7 @@ public void testServiceAccountRequired() throws Exception { try { FirebaseAuth.getInstance(app).createCustomTokenAsync("foo").get(); fail("Expected exception."); - } catch (IllegalStateException expected) { + } catch (IllegalArgumentException expected) { Assert.assertEquals( "Must initialize FirebaseApp with a service account credential to call " + "createCustomToken()", @@ -353,7 +319,7 @@ public void testProjectIdRequired() throws Exception { try { FirebaseAuth.getInstance(app).verifyIdTokenAsync("foo").get(); fail("Expected exception."); - } catch (IllegalStateException expected) { + } catch (IllegalArgumentException expected) { Assert.assertEquals( "Must initialize FirebaseApp with a project ID to call verifyIdToken()", expected.getMessage()); diff --git a/src/test/java/com/google/firebase/auth/FirebaseCredentialsTest.java b/src/test/java/com/google/firebase/auth/FirebaseCredentialsTest.java deleted file mode 100644 index dd52e941a..000000000 --- a/src/test/java/com/google/firebase/auth/FirebaseCredentialsTest.java +++ /dev/null @@ -1,292 +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.auth; - -import com.google.api.client.googleapis.testing.auth.oauth2.MockTokenServerTransport; -import com.google.api.client.googleapis.util.Utils; -import com.google.api.client.json.JsonFactory; -import com.google.auth.oauth2.AccessToken; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.firebase.auth.internal.BaseCredential; -import com.google.firebase.auth.internal.FirebaseCredentialsAdapter; -import com.google.firebase.tasks.Task; -import com.google.firebase.tasks.Tasks; -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.nio.charset.Charset; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -/** - * Tests for {@link FirebaseCredentials}. - */ -public class FirebaseCredentialsTest { - - 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(); - - @Rule public final ExpectedException thrown = ExpectedException.none(); - - @BeforeClass - public static void setupClass() throws IOException { - TestUtils.getApplicationDefaultCredentials(); - } - - @Test(expected = NullPointerException.class) - public void testNullCertificate() throws IOException { - FirebaseCredentials.fromCertificate(null); - } - - @Test(expected = NullPointerException.class) - public void testNullRefreshToken() throws IOException { - FirebaseCredentials.fromRefreshToken(null); - } - - @Test - public void defaultCredentialDoesntRefetch() throws Exception { - FirebaseCredential credential = FirebaseCredentials.applicationDefault( - Utils.getDefaultTransport(), Utils.getDefaultJsonFactory()); - GoogleOAuthAccessToken token = Tasks.await(credential.getAccessToken()); - Assert.assertEquals(TestUtils.TEST_ADC_ACCESS_TOKEN, token.getAccessToken()); - Assert.assertNotNull(((BaseCredential) credential).getGoogleCredentials()); - - // We should still be able to fetch the token since the certificate is cached - credential = FirebaseCredentials.applicationDefault(); - token = Tasks.await(credential.getAccessToken()); - Assert.assertNotNull(token); - Assert.assertEquals(TestUtils.TEST_ADC_ACCESS_TOKEN, token.getAccessToken()); - Assert.assertNotNull(((BaseCredential) credential).getGoogleCredentials()); - } - - @Test - public void canResolveTokenMoreThanOnce() - throws ExecutionException, InterruptedException, IOException { - MockTokenServerTransport transport = new MockTokenServerTransport(); - transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), ACCESS_TOKEN); - - FirebaseCredential credential = - FirebaseCredentials.fromCertificate( - ServiceAccount.EDITOR.asStream(), transport, Utils.getDefaultJsonFactory()); - - Tasks.await(credential.getAccessToken()); - Tasks.await(credential.getAccessToken()); - } - - @Test - public void certificateReadIsDoneSynchronously() - throws ExecutionException, InterruptedException, IOException { - MockTokenServerTransport transport = new MockTokenServerTransport(); - transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), ACCESS_TOKEN); - - ByteArrayInputStream inputStream = - new ByteArrayInputStream( - ServiceAccount.EDITOR.asString().getBytes(Charset.defaultCharset())); - - FirebaseCredential credential = - FirebaseCredentials.fromCertificate(inputStream, transport, Utils.getDefaultJsonFactory()); - - Assert.assertEquals(0, inputStream.available()); - inputStream.close(); - - Assert.assertNotNull(((BaseCredential) credential).getGoogleCredentials()); - Assert.assertEquals(ACCESS_TOKEN, Tasks.await(credential.getAccessToken()).getAccessToken()); - } - - @Test - public void certificateReadChecksForProjectId() - throws ExecutionException, InterruptedException, IOException { - MockTokenServerTransport transport = new MockTokenServerTransport(); - transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), ACCESS_TOKEN); - - String accountWithoutProjectId = - ServiceAccount.EDITOR.asString().replace("project_id", "missing"); - ByteArrayInputStream inputStream = - new ByteArrayInputStream(accountWithoutProjectId.getBytes(Charset.defaultCharset())); - - try { - FirebaseCredentials.fromCertificate(inputStream, transport, Utils.getDefaultJsonFactory()); - Assert.fail(); - } catch (IllegalArgumentException e) { - Assert.assertEquals( - "Failed to parse service account: 'project_id' must be set", e.getMessage()); - } - } - - @Test - public void certificateReadThrowsIOException() - throws ExecutionException, InterruptedException { - MockTokenServerTransport transport = new MockTokenServerTransport(); - transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), ACCESS_TOKEN); - - InputStream inputStream = - new InputStream() { - @Override - public int read() throws IOException { - throw new IOException("Expected"); - } - }; - - - try { - FirebaseCredentials.fromCertificate(inputStream, transport, Utils.getDefaultJsonFactory()); - Assert.fail(); - } catch (IOException e) { - Assert.assertEquals("Expected", e.getMessage()); - } - } - - @Test - public void nullThrowsRuntimeExceptionFromCertificate() - throws ExecutionException, InterruptedException, IOException { - final MockTokenServerTransport transport = new MockTokenServerTransport(); - transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), ACCESS_TOKEN); - thrown.expect(NullPointerException.class); - FirebaseCredentials.fromCertificate(null, transport, Utils.getDefaultJsonFactory()); - } - - @Test - public void nullThrowsRuntimeExceptionFromRefreshToken() - throws ExecutionException, InterruptedException, IOException { - final MockTokenServerTransport transport = new MockTokenServerTransport(); - transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), ACCESS_TOKEN); - thrown.expect(NullPointerException.class); - FirebaseCredentials.fromRefreshToken(null, transport, Utils.getDefaultJsonFactory()); - } - - @Test - public void refreshTokenReadIsDoneSynchronously() - throws ExecutionException, InterruptedException, IOException { - 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 inputStream = new ByteArrayInputStream(JSON_FACTORY.toByteArray(secretJson)); - - FirebaseCredential credential = - FirebaseCredentials.fromRefreshToken(inputStream, transport, Utils.getDefaultJsonFactory()); - - Assert.assertEquals(0, inputStream.available()); - inputStream.close(); - - Assert.assertNotNull(((BaseCredential) credential).getGoogleCredentials()); - Assert.assertEquals(ACCESS_TOKEN, Tasks.await(credential.getAccessToken()).getAccessToken()); - } - - @Test - public void refreshTokenReadThrowsIOException() - throws ExecutionException, InterruptedException { - MockTokenServerTransport transport = new MockTokenServerTransport(); - transport.addServiceAccount(ServiceAccount.EDITOR.getEmail(), ACCESS_TOKEN); - - InputStream inputStream = - new InputStream() { - @Override - public int read() throws IOException { - throw new IOException("Expected"); - } - }; - - try { - FirebaseCredentials.fromRefreshToken(inputStream, transport, Utils.getDefaultJsonFactory()); - Assert.fail(); - } catch (IOException e) { - Assert.assertEquals("Expected", e.getMessage()); - } - } - - @Test(expected = Exception.class) - public void serviceAccountUsedAsRefreshToken() throws Exception { - FirebaseCredentials.fromRefreshToken(ServiceAccount.EDITOR.asStream()); - } - - @Test - public void tokenNotCached() throws Exception { - TestCredential credential = new TestCredential(new MockGoogleCredentials(ACCESS_TOKEN, 10L)); - - for (long i = 0; i < 10; i++) { - Assert.assertEquals(ACCESS_TOKEN, Tasks.await(credential.getAccessToken()).getAccessToken()); - Assert.assertEquals(i + 1, credential.getFetchCount()); - } - } - - @Test - public void testTokenExpiration() throws Exception { - final MockGoogleCredentials googleCredentials = new MockGoogleCredentials(ACCESS_TOKEN); - TestCredential credential = new TestCredential(googleCredentials); - - for (long i = 0; i < 10; i++) { - long expiryTime = (i + 1) * 10L; - googleCredentials.setExpiryTime(expiryTime); - GoogleOAuthAccessToken googleToken = Tasks.await(credential.getAccessToken()); - Assert.assertEquals(ACCESS_TOKEN, googleToken.getAccessToken()); - Assert.assertEquals(expiryTime, googleToken.getExpiryTime()); - } - } - - @Test - public void testCustomFirebaseCredential() throws IOException { - final Date date = new Date(); - FirebaseCredential credential = new FirebaseCredential() { - @Override - public Task getAccessToken() { - return Tasks.forResult(new GoogleOAuthAccessToken("token", date.getTime())); - } - }; - GoogleCredentials googleCredentials = new FirebaseCredentialsAdapter(credential); - AccessToken accessToken = googleCredentials.refreshAccessToken(); - Assert.assertEquals("token", accessToken.getTokenValue()); - Assert.assertEquals(date, accessToken.getExpirationTime()); - } - - private static class TestCredential extends BaseCredential { - - private final AtomicInteger fetchCount = new AtomicInteger(0); - - TestCredential(GoogleCredentials googleCredentials) { - super(googleCredentials); - } - - @Override - public Task getAccessToken() { - fetchCount.incrementAndGet(); - return super.getAccessToken(); - } - - int getFetchCount() { - return fetchCount.get(); - } - } -} diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 25f5cde6c..b10058420 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -391,7 +391,7 @@ public void call(FirebaseAuth auth) throws Exception { operation.call(FirebaseAuth.getInstance()); fail("No error thrown for HTTP error: " + code); } catch (ExecutionException e) { - assertTrue(e.getCause().toString(), e.getCause() instanceof FirebaseAuthException); + assertTrue(e.getCause() instanceof FirebaseAuthException); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); String msg = String.format("Unexpected HTTP response with status: %d; body: {}", code); assertEquals(msg, authException.getMessage()); diff --git a/src/test/java/com/google/firebase/database/TestHelpers.java b/src/test/java/com/google/firebase/database/TestHelpers.java index da05f8adf..cfd15034d 100644 --- a/src/test/java/com/google/firebase/database/TestHelpers.java +++ b/src/test/java/com/google/firebase/database/TestHelpers.java @@ -20,7 +20,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.database.connection.ConnectionAuthTokenProvider; import com.google.firebase.database.connection.ConnectionContext; @@ -31,8 +30,6 @@ import com.google.firebase.database.core.RepoManager; import com.google.firebase.database.core.view.QuerySpec; import com.google.firebase.database.future.WriteFuture; -import com.google.firebase.database.logging.DefaultLogger; -import com.google.firebase.database.logging.Logger.Level; import com.google.firebase.database.snapshot.ChildKey; import com.google.firebase.database.util.JsonMapper; import com.google.firebase.database.utilities.DefaultRunLoop; @@ -67,7 +64,6 @@ public static DatabaseConfig newFrozenTestConfig(FirebaseApp app) { public static DatabaseConfig newTestConfig(FirebaseApp app) { DatabaseConfig config = new DatabaseConfig(); - config.setLogLevel(Logger.Level.WARN); config.setFirebaseApp(app); return config; } @@ -94,11 +90,6 @@ public static ScheduledExecutorService getExecutorService(DatabaseConfig config) return runLoop.getExecutorService(); } - public static void setLogger( - DatabaseConfig ctx, com.google.firebase.database.logging.Logger logger) { - ctx.setLogger(logger); - } - public static void waitFor(Semaphore semaphore) throws InterruptedException { waitFor(semaphore, 1); } @@ -323,15 +314,13 @@ public Object answer(InvocationOnMock invocation) throws Throwable { } public static ConnectionContext newConnectionContext(ScheduledExecutorService executor) { - com.google.firebase.database.logging.Logger logger = new DefaultLogger( - Level.NONE, ImmutableList.of()); ConnectionAuthTokenProvider tokenProvider = new ConnectionAuthTokenProvider() { @Override public void getToken(boolean forceRefresh, GetTokenCallback callback) { callback.onSuccess("gauth|{\"token\":\"test-token\"}"); } }; - return new ConnectionContext(logger, tokenProvider, executor, false, "testVersion", + return new ConnectionContext(tokenProvider, executor, false, "testVersion", "testUserAgent", Executors.defaultThreadFactory()); } diff --git a/src/test/java/com/google/firebase/database/connection/ConnectionTest.java b/src/test/java/com/google/firebase/database/connection/ConnectionTest.java index 56ff9a95a..4e413fb48 100644 --- a/src/test/java/com/google/firebase/database/connection/ConnectionTest.java +++ b/src/test/java/com/google/firebase/database/connection/ConnectionTest.java @@ -17,7 +17,6 @@ package com.google.firebase.database.connection; import com.google.common.collect.ImmutableMap; -import com.google.firebase.database.TestHelpers; import com.google.firebase.database.connection.Connection.Delegate; import com.google.firebase.database.connection.Connection.DisconnectReason; import com.google.firebase.database.connection.Connection.WebsocketConnectionFactory; @@ -187,7 +186,6 @@ private static class MockConnectionFactory implements WebsocketConnectionFactory MockConnectionFactory() { this.connection = new Connection( - TestHelpers.newConnectionContext(executor), Mockito.mock(HostInfo.class), delegate, this); diff --git a/src/test/java/com/google/firebase/database/core/GaePlatformTest.java b/src/test/java/com/google/firebase/database/core/GaePlatformTest.java deleted file mode 100644 index 032de1b4a..000000000 --- a/src/test/java/com/google/firebase/database/core/GaePlatformTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.google.firebase.database.core; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import com.google.common.collect.ImmutableList; -import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseOptions; -import com.google.firebase.TestOnlyImplFirebaseTrampolines; -import com.google.firebase.ThreadManager; -import com.google.firebase.database.FirebaseDatabase; -import com.google.firebase.database.logging.Logger; -import com.google.firebase.internal.GaeThreadFactory; -import com.google.firebase.internal.NonNull; -import com.google.firebase.testing.ServiceAccount; -import com.google.firebase.testing.TestUtils; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Test; -import org.mockito.Mockito; - -public class GaePlatformTest { - - @Test - public void testIsActive() { - assertEquals(GaeThreadFactory.isAvailable(), GaePlatform.isActive()); - } - - @Test - public void testGaePlatform() { - final AtomicInteger count = new AtomicInteger(0); - final ThreadManager threadManager = new ThreadManager() { - @Override - protected ExecutorService getExecutor(@NonNull FirebaseApp app) { - return Executors.newSingleThreadExecutor(); - } - - @Override - protected void releaseExecutor(@NonNull FirebaseApp app, - @NonNull ExecutorService executor) { - } - - @Override - protected ThreadFactory getThreadFactory() { - count.incrementAndGet(); - return Executors.defaultThreadFactory(); - } - }; - - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) - .setThreadManager(threadManager) - .build(); - FirebaseApp app = FirebaseApp.initializeApp(options, "gaeApp"); - try { - GaePlatform platform = new GaePlatform(app); - Context ctx = Mockito.mock(Context.class); - assertNotNull(platform.newLogger(ctx, Logger.Level.DEBUG, ImmutableList.of())); - - assertNotNull(platform.newEventTarget(ctx)); - assertEquals(1, count.get()); - - assertNotNull(platform.newRunLoop(ctx)); - assertEquals(2, count.get()); - - AuthTokenProvider authTokenProvider = platform.newAuthTokenProvider( - Mockito.mock(ScheduledExecutorService.class)); - assertTrue(authTokenProvider instanceof JvmAuthTokenProvider); - - assertEquals("AppEngine/AdminJava", platform.getUserAgent(ctx)); - assertEquals("gae-" + FirebaseDatabase.getSdkVersion(), platform.getPlatformVersion()); - assertNull(platform.createPersistenceManager(ctx, "test")); - - ThreadInitializer threadInitializer = platform.getThreadInitializer(); - Thread t = new Thread(); - threadInitializer.setName(t, "test-name"); - threadInitializer.setDaemon(t, true); - threadInitializer.setUncaughtExceptionHandler(t, Mockito.mock( - Thread.UncaughtExceptionHandler.class)); - assertNotEquals("test-name", t.getName()); - assertFalse(t.isDaemon()); - assertNotNull(t.getUncaughtExceptionHandler()); - - } finally { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); - } - } -} diff --git a/src/test/java/com/google/firebase/database/core/SyncPointTest.java b/src/test/java/com/google/firebase/database/core/SyncPointTest.java index 4d70fd06b..91be0cce7 100644 --- a/src/test/java/com/google/firebase/database/core/SyncPointTest.java +++ b/src/test/java/com/google/firebase/database/core/SyncPointTest.java @@ -28,7 +28,6 @@ import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.InternalHelpers; import com.google.firebase.database.Query; -import com.google.firebase.database.TestHelpers; import com.google.firebase.database.annotations.NotNull; import com.google.firebase.database.connection.ListenHashProvider; import com.google.firebase.database.core.persistence.NoopPersistenceManager; @@ -37,9 +36,6 @@ import com.google.firebase.database.core.view.DataEvent; import com.google.firebase.database.core.view.Event; import com.google.firebase.database.core.view.QuerySpec; -import com.google.firebase.database.logging.DefaultLogger; -import com.google.firebase.database.logging.LogWrapper; -import com.google.firebase.database.logging.Logger; import com.google.firebase.database.snapshot.IndexedNode; import com.google.firebase.database.snapshot.Node; import com.google.firebase.database.snapshot.NodeUtilities; @@ -60,9 +56,13 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class SyncPointTest { + private static final Logger logger = LoggerFactory.getLogger(SyncPointTest.class); + private static FirebaseApp testApp; @BeforeClass @@ -79,7 +79,7 @@ public static void tearDownClass() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); } - private static SyncTree.ListenProvider getNewListenProvider(final LogWrapper logger) { + private static SyncTree.ListenProvider getNewListenProvider() { return new SyncTree.ListenProvider() { private final HashSet listens = new HashSet<>(); @@ -96,7 +96,7 @@ public void startListening( @Override public void stopListening(QuerySpec query, Tag tag) { Path path = query.getPath(); - logger.debug("Listening at " + path + " for Tag " + tag); + logger.debug("Listening at {} for Tag {}", path, tag); checkState(this.listens.contains(query), "Stopped listening for query already"); this.listens.remove(query); @@ -314,12 +314,9 @@ private static Map parseMergePaths(Map merges) { @SuppressWarnings("unchecked") private static void runTest(Map testSpec, String basePath) { - DatabaseConfig config = TestHelpers.newTestConfig(testApp); - TestHelpers.setLogger(config, new DefaultLogger(Logger.Level.WARN, null)); - LogWrapper logger = config.getLogger("SyncPointTest"); - logger.info("Running \"" + testSpec.get("name") + '"'); - SyncTree.ListenProvider listenProvider = getNewListenProvider(logger); - SyncTree syncTree = new SyncTree(config, new NoopPersistenceManager(), listenProvider); + logger.debug("Running \"{}\"", testSpec.get("name")); + SyncTree.ListenProvider listenProvider = getNewListenProvider(); + SyncTree syncTree = new SyncTree(new NoopPersistenceManager(), listenProvider); int currentWriteId = 0; diff --git a/src/test/java/com/google/firebase/database/core/persistence/DefaultPersistenceManagerTest.java b/src/test/java/com/google/firebase/database/core/persistence/DefaultPersistenceManagerTest.java index b06cfb841..99450944c 100644 --- a/src/test/java/com/google/firebase/database/core/persistence/DefaultPersistenceManagerTest.java +++ b/src/test/java/com/google/firebase/database/core/persistence/DefaultPersistenceManagerTest.java @@ -70,7 +70,7 @@ public static void tearDownClass() { private PersistenceManager newTestPersistenceManager() { MockPersistenceStorageEngine engine = new MockPersistenceStorageEngine(); engine.disableTransactionCheck = true; - return new DefaultPersistenceManager(newFrozenTestConfig(testApp), engine, CachePolicy.NONE); + return new DefaultPersistenceManager(engine, CachePolicy.NONE); } @Test @@ -131,7 +131,7 @@ public void applyUserMergeUsesRelativePath() { engine.overwriteServerCache(path(""), initialData); DefaultPersistenceManager manager = - new DefaultPersistenceManager(newFrozenTestConfig(testApp), engine, CachePolicy.NONE); + new DefaultPersistenceManager(engine, CachePolicy.NONE); CompoundWrite write = CompoundWrite.fromValue(fromSingleQuotedString("{'baz': 'new-baz', 'qux': 'qux'}")); diff --git a/src/test/java/com/google/firebase/database/core/persistence/RandomPersistenceTest.java b/src/test/java/com/google/firebase/database/core/persistence/RandomPersistenceTest.java index ce9a641db..58358a8ff 100644 --- a/src/test/java/com/google/firebase/database/core/persistence/RandomPersistenceTest.java +++ b/src/test/java/com/google/firebase/database/core/persistence/RandomPersistenceTest.java @@ -181,10 +181,10 @@ public void randomOperations() { DatabaseConfig cfg = TestHelpers.newFrozenTestConfig(testApp); MockPersistenceStorageEngine storageEngine = new MockPersistenceStorageEngine(); DefaultPersistenceManager manager = - new DefaultPersistenceManager(cfg, storageEngine, CachePolicy.NONE); + new DefaultPersistenceManager(storageEngine, CachePolicy.NONE); final HashMap tagMap = new HashMap<>(); SyncTree syncTree = - new SyncTree(cfg, manager, new SyncTree.ListenProvider() { + new SyncTree(manager, new SyncTree.ListenProvider() { @Override public void startListening( QuerySpec query, diff --git a/src/test/java/com/google/firebase/database/core/persistence/TrackedQueryManagerTest.java b/src/test/java/com/google/firebase/database/core/persistence/TrackedQueryManagerTest.java index 66b5a515f..8b293cedf 100644 --- a/src/test/java/com/google/firebase/database/core/persistence/TrackedQueryManagerTest.java +++ b/src/test/java/com/google/firebase/database/core/persistence/TrackedQueryManagerTest.java @@ -31,14 +31,12 @@ import com.google.firebase.database.core.utilities.TestClock; import com.google.firebase.database.core.view.QueryParams; import com.google.firebase.database.core.view.QuerySpec; -import com.google.firebase.database.logging.DefaultLogger; -import com.google.firebase.database.logging.LogWrapper; -import com.google.firebase.database.logging.Logger; import com.google.firebase.database.snapshot.ChildKey; import com.google.firebase.database.snapshot.PathIndex; import com.google.firebase.database.utilities.Clock; import java.util.Collections; import org.junit.Test; +import org.slf4j.LoggerFactory; public class TrackedQueryManagerTest { @@ -66,9 +64,8 @@ private TrackedQueryManager newManager(PersistenceStorageEngine engine, Clock cl e.disableTransactionCheck = true; engine = e; } - LogWrapper logWrapper = - new LogWrapper(new DefaultLogger(Logger.Level.WARN, null), TrackedQueryManagerTest.class); - return new TrackedQueryManager(engine, logWrapper, clock); + return new TrackedQueryManager( + engine, LoggerFactory.getLogger(TrackedQueryManagerTest.class), clock); } @Test diff --git a/src/test/java/com/google/firebase/database/integration/ShutdownExample.java b/src/test/java/com/google/firebase/database/integration/ShutdownExample.java index 68b6ac384..020e0416e 100644 --- a/src/test/java/com/google/firebase/database/integration/ShutdownExample.java +++ b/src/test/java/com/google/firebase/database/integration/ShutdownExample.java @@ -22,7 +22,6 @@ import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.FirebaseDatabase; -import com.google.firebase.database.Logger.Level; import com.google.firebase.database.ValueEventListener; import java.util.concurrent.Semaphore; @@ -38,7 +37,6 @@ public static void main(String[] args) { .build()); FirebaseDatabase db = FirebaseDatabase.getInstance(app); - db.setLogLevel(Level.DEBUG); DatabaseReference ref = db.getReference(); ValueEventListener listener = diff --git a/src/test/java/com/google/firebase/database/utilities/DefaultRunLoopTest.java b/src/test/java/com/google/firebase/database/utilities/DefaultRunLoopTest.java index 1029ebef8..065612568 100644 --- a/src/test/java/com/google/firebase/database/utilities/DefaultRunLoopTest.java +++ b/src/test/java/com/google/firebase/database/utilities/DefaultRunLoopTest.java @@ -38,7 +38,7 @@ public class DefaultRunLoopTest { public void testLifecycle() { MockRunLoop runLoop = new MockRunLoop(); try { - assertEquals(0, runLoop.getThreadPool().getCorePoolSize()); + assertEquals(1, runLoop.getThreadPool().getCorePoolSize()); runLoop.scheduleNow(new Runnable() { @Override public void run() { @@ -62,7 +62,7 @@ public void run() { public void testScheduleWithDelay() throws ExecutionException, InterruptedException { MockRunLoop runLoop = new MockRunLoop(); try { - assertEquals(0, runLoop.getThreadPool().getCorePoolSize()); + assertEquals(1, runLoop.getThreadPool().getCorePoolSize()); ScheduledFuture future = runLoop.schedule(new Runnable() { @Override public void run() { @@ -91,7 +91,7 @@ public void uncaughtException(Thread t, Throwable e) { assertSame(exceptionHandler, runLoop.getExceptionHandler()); try { - assertEquals(0, runLoop.getThreadPool().getCorePoolSize()); + assertEquals(1, runLoop.getThreadPool().getCorePoolSize()); runLoop.scheduleNow(new Runnable() { @Override public void run() { diff --git a/src/test/java/com/google/firebase/database/utilities/UtilitiesTest.java b/src/test/java/com/google/firebase/database/utilities/UtilitiesTest.java index 3af81179b..f4efbad57 100644 --- a/src/test/java/com/google/firebase/database/utilities/UtilitiesTest.java +++ b/src/test/java/com/google/firebase/database/utilities/UtilitiesTest.java @@ -25,13 +25,14 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.api.core.ApiFuture; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseException; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.DatabaseReference.CompletionListener; -import com.google.firebase.tasks.Task; +import java.util.concurrent.ExecutionException; import org.junit.Test; public class UtilitiesTest { @@ -142,37 +143,32 @@ public void testHardAssert() { } @Test - public void testWrapOnComplete() { - Pair, DatabaseReference.CompletionListener> result = + public void testWrapOnComplete() throws Exception { + Pair, DatabaseReference.CompletionListener> result = Utilities.wrapOnComplete(null); assertNotNull(result.getFirst()); assertNotNull(result.getSecond()); - try { - result.getFirst().getResult(); - fail("No error thrown for pending task"); - } catch (Exception e) { - // expected - } + assertFalse(result.getFirst().isDone()); result.getSecond().onComplete(null, null); - result.getFirst().getResult(); + assertTrue(result.getFirst().isDone()); + assertNull(result.getFirst().get()); } @Test - public void testWrapOnCompleteErrorResult() { - Pair, DatabaseReference.CompletionListener> result = + public void testWrapOnCompleteErrorResult() throws InterruptedException { + Pair, DatabaseReference.CompletionListener> result = Utilities.wrapOnComplete(null); assertNotNull(result.getFirst()); assertNotNull(result.getSecond()); - try { - result.getFirst().getResult(); - fail("No error thrown for pending task"); - } catch (Exception e) { - // expected - } + assertFalse(result.getFirst().isDone()); result.getSecond().onComplete(DatabaseError.fromStatus("test error"), null); - assertNotNull(result.getFirst().getException()); + try { + result.getFirst().get(); + } catch (ExecutionException e) { + assertNotNull(e.getCause()); + } } @Test @@ -183,7 +179,7 @@ public void onComplete(DatabaseError error, DatabaseReference ref) { } }; - Pair, DatabaseReference.CompletionListener> result = + Pair, DatabaseReference.CompletionListener> result = Utilities.wrapOnComplete(listener); assertNull(result.getFirst()); assertSame(listener, result.getSecond()); diff --git a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java index 549fb05b1..366bb7e29 100644 --- a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java +++ b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java @@ -27,12 +27,15 @@ import com.google.api.client.http.HttpResponseException; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; +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.GenericFunction; import com.google.firebase.testing.TestResponseInterceptor; +import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import org.junit.After; @@ -60,7 +63,7 @@ public void testNoProjectId() { } @Test - public void testInvalidInstanceId() throws Exception { + public void testInvalidInstanceId() { FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") @@ -100,19 +103,38 @@ public void testDeleteInstanceId() throws Exception { .build(); FirebaseApp app = FirebaseApp.initializeApp(options); - FirebaseInstanceId instanceId = FirebaseInstanceId.getInstance(); + final FirebaseInstanceId instanceId = FirebaseInstanceId.getInstance(); assertSame(instanceId, FirebaseInstanceId.getInstance(app)); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - instanceId.setInterceptor(interceptor); - instanceId.deleteInstanceIdAsync("test-iid").get(); + List> functions = ImmutableList.of( + new GenericFunction() { + @Override + public Void call(Object... args) throws Exception { + instanceId.deleteInstanceIdAsync("test-iid").get(); + return null; + } + }, + new GenericFunction() { + @Override + public Void call(Object... args) throws Exception { + instanceId.deleteInstanceId("test-iid"); + return null; + } + } + ); - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("DELETE", request.getRequestMethod()); String url = "https://console.firebase.google.com/v1/project/test-project/instanceId/test-iid"; - assertEquals(url, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + for (GenericFunction fn : functions) { + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + instanceId.setInterceptor(interceptor); + fn.call(); + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals("DELETE", request.getRequestMethod()); + assertEquals(url, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + } } @Test diff --git a/src/test/java/com/google/firebase/internal/CallableOperationTest.java b/src/test/java/com/google/firebase/internal/CallableOperationTest.java new file mode 100644 index 000000000..2ead9a9c7 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/CallableOperationTest.java @@ -0,0 +1,106 @@ +/* + * 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.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +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.auth.MockGoogleCredentials; +import com.google.firebase.internal.FirebaseThreadManagers.GlobalThreadManager; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import org.junit.After; +import org.junit.Test; + +public class CallableOperationTest { + + private static final String TEST_FIREBASE_THREAD = "test-firebase-thread"; + private static final FirebaseOptions OPTIONS = new Builder() + .setCredentials(new MockGoogleCredentials()) + .setThreadManager(new MockThreadManager()) + .build(); + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testCallResult() throws Exception { + FirebaseApp app = FirebaseApp.initializeApp(OPTIONS); + CallableOperation operation = new CallableOperation() { + @Override + protected Boolean execute() throws Exception { + String threadName = Thread.currentThread().getName(); + return TEST_FIREBASE_THREAD.equals(threadName); + } + }; + assertFalse(operation.call()); + assertTrue(operation.callAsync(app).get()); + } + + @Test + public void testCallException() throws Exception { + FirebaseApp app = FirebaseApp.initializeApp(OPTIONS); + CallableOperation operation = new CallableOperation() { + @Override + protected Boolean execute() throws Exception { + String threadName = Thread.currentThread().getName(); + if (TEST_FIREBASE_THREAD.equals(threadName)) { + throw new Exception(threadName); + } + return false; + } + }; + + assertFalse(operation.call()); + try { + operation.callAsync(app).get(); + fail("No exception thrown"); + } catch (ExecutionException e) { + assertEquals(TEST_FIREBASE_THREAD, e.getCause().getMessage()); + } + } + + private static class MockThreadManager extends GlobalThreadManager { + @Override + protected ExecutorService doInit() { + return Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() + .setNameFormat(TEST_FIREBASE_THREAD) + .build()); + } + + @Override + protected void doCleanup(ExecutorService executorService) { + executorService.shutdownNow(); + } + + @Override + protected ThreadFactory getThreadFactory() { + return Executors.defaultThreadFactory(); + } + } +} diff --git a/src/test/java/com/google/firebase/internal/FirebaseThreadManagersTest.java b/src/test/java/com/google/firebase/internal/FirebaseThreadManagersTest.java index 7a0d2d104..2b117f29f 100644 --- a/src/test/java/com/google/firebase/internal/FirebaseThreadManagersTest.java +++ b/src/test/java/com/google/firebase/internal/FirebaseThreadManagersTest.java @@ -29,7 +29,6 @@ import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.internal.FirebaseThreadManagers.GlobalThreadManager; -import com.google.firebase.tasks.Tasks; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; @@ -38,7 +37,6 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadFactory; import org.junit.After; -import org.junit.Assume; import org.junit.Test; public class FirebaseThreadManagersTest { @@ -127,8 +125,6 @@ public void testGlobalThreadManagerReInit() { @Test public void testDefaultThreadManager() throws Exception { - Assume.assumeFalse(GaeThreadFactory.isAvailable()); - FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(new MockGoogleCredentials()) .build(); @@ -143,7 +139,7 @@ public Void call() throws Exception { return null; } }; - Tasks.await(ImplFirebaseTrampolines.submitCallable(defaultApp, command)); + ImplFirebaseTrampolines.submitCallable(defaultApp, command).get(); // Check for default JVM thread properties. assertTrue(threadInfo.get("name").toString().startsWith("firebase-default-")); diff --git a/src/test/java/com/google/firebase/internal/GaeExecutorServiceTest.java b/src/test/java/com/google/firebase/internal/GaeExecutorServiceTest.java deleted file mode 100644 index 48b2678cd..000000000 --- a/src/test/java/com/google/firebase/internal/GaeExecutorServiceTest.java +++ /dev/null @@ -1,129 +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.internal; - -import static com.google.firebase.database.TestHelpers.waitFor; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import com.google.common.collect.ImmutableList; -import com.google.firebase.testing.TestUtils; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.Semaphore; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -public class GaeExecutorServiceTest { - - @Test - public void testShutdownBeforeUse() throws InterruptedException { - CountingThreadFactory threadFactory = new CountingThreadFactory(); - GaeExecutorService executorService = new GaeExecutorService("test", threadFactory); - assertFalse(executorService.isShutdown()); - assertFalse(executorService.isTerminated()); - - assertEquals(ImmutableList.of(), executorService.shutdownNow()); - assertTrue(executorService.isShutdown()); - assertTrue(executorService.isTerminated()); - assertTrue(executorService.awaitTermination(1, TimeUnit.SECONDS)); - assertEquals(0, threadFactory.counter.get()); - - executorService = new GaeExecutorService("test", threadFactory); - assertFalse(executorService.isShutdown()); - assertFalse(executorService.isTerminated()); - - executorService.shutdownNow(); - assertTrue(executorService.isShutdown()); - assertTrue(executorService.isTerminated()); - assertTrue(executorService.awaitTermination( - TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertEquals(0, threadFactory.counter.get()); - } - - @Test - public void testSubmit() throws InterruptedException, ExecutionException { - CountingThreadFactory threadFactory = new CountingThreadFactory(); - GaeExecutorService executorService = new GaeExecutorService("test", threadFactory); - - final Semaphore semaphore = new Semaphore(0); - Future future = executorService.submit(new Runnable() { - @Override - public void run() { - semaphore.release(); - } - }); - assertNotNull(future); - waitFor(semaphore); - - future = executorService.submit(new Runnable() { - @Override - public void run() { - semaphore.release(); - } - }, "result"); - assertNotNull(future); - waitFor(semaphore); - assertEquals("result", future.get()); - - future = executorService.submit(new Callable() { - @Override - public Object call() throws Exception { - semaphore.release(); - return "result2"; - } - }); - assertNotNull(future); - waitFor(semaphore); - assertEquals("result2", future.get()); - - executorService.execute(new Runnable() { - @Override - public void run() { - semaphore.release(); - } - }); - waitFor(semaphore); - - assertEquals(4, threadFactory.counter.get()); - - executorService.shutdown(); - assertTrue(executorService.awaitTermination( - TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertTrue(executorService.isShutdown()); - assertTrue(executorService.isTerminated()); - } - - private static class CountingThreadFactory implements ThreadFactory { - - private final AtomicInteger counter = new AtomicInteger(0); - private final ThreadFactory delegate = Executors.defaultThreadFactory(); - - @Override - public Thread newThread(Runnable r) { - counter.incrementAndGet(); - return delegate.newThread(r); - } - } -} diff --git a/src/test/java/com/google/firebase/internal/RevivingScheduledExecutorTest.java b/src/test/java/com/google/firebase/internal/RevivingScheduledExecutorTest.java deleted file mode 100644 index 3a4da947c..000000000 --- a/src/test/java/com/google/firebase/internal/RevivingScheduledExecutorTest.java +++ /dev/null @@ -1,238 +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.internal; - -import java.lang.Thread.UncaughtExceptionHandler; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.Semaphore; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Assert; -import org.junit.Test; - -public class RevivingScheduledExecutorTest { - - private static final ThreadFactory THREAD_FACTORY = new ExceptionCatchingThreadFactory(); - - @Test - public void testAppEngineRunnable() throws InterruptedException { - final Semaphore semaphore = new Semaphore(0); - final Set threadIds = new HashSet<>(); - - RevivingScheduledExecutor executor = - new RevivingScheduledExecutor(THREAD_FACTORY, "testAppEngineRunnable", 0, 100); - - for (int i = 0; i < 50; ++i) { - // We delay the execution to give the cleanup handler a chance to run. Otherwise, the - // Executor's BlockingQueue will execute all Runnables before the internal thread gets - // replaced. - Thread.sleep(10); - executor.execute( - new Runnable() { - @Override - public void run() { - threadIds.add(Thread.currentThread().getId()); - semaphore.release(); - } - }); - } - - try { - Assert.assertTrue(semaphore.tryAcquire(50, 10, TimeUnit.SECONDS)); - Assert.assertTrue(threadIds.size() > 1); - } finally { - executor.shutdownNow(); - } - } - - @Test - public void testAppEnginePeriodicRunnable() throws InterruptedException { - final Set threadIds = new HashSet<>(); - final Semaphore semaphore = new Semaphore(0); - - RevivingScheduledExecutor executor = - new RevivingScheduledExecutor(THREAD_FACTORY, "testAppEnginePeriodicRunnable", 0, 100); - - ScheduledFuture future = - executor.scheduleAtFixedRate( - new Runnable() { - @Override - public void run() { - threadIds.add(Thread.currentThread().getId()); - semaphore.release(); - } - }, - 0, - 10, - TimeUnit.MILLISECONDS); - - try { - Assert.assertTrue(semaphore.tryAcquire(50, 10, TimeUnit.SECONDS)); - Assert.assertTrue(threadIds.size() > 1); - } finally { - future.cancel(true); - executor.shutdownNow(); - } - } - - @Test - public void testAppEngineDelayedRunnable() throws InterruptedException { - final Semaphore semaphore = new Semaphore(0); - final AtomicInteger threads = new AtomicInteger(0); - - RevivingScheduledExecutor executor = - new RevivingScheduledExecutor(new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - threads.incrementAndGet(); - return THREAD_FACTORY.newThread(r); - } - }, - "testAppEngineDelayedRunnable", - 0, - 100); - - @SuppressWarnings("unused") - Future possiblyIgnoredError = - executor.schedule( - new Runnable() { - @Override - public void run() { - semaphore.release(); - } - }, - 750, - TimeUnit.MILLISECONDS); - - try { - Assert.assertFalse(semaphore.tryAcquire(1, 500, TimeUnit.MILLISECONDS)); - Assert.assertTrue(semaphore.tryAcquire(1, 500, TimeUnit.MILLISECONDS)); - Assert.assertTrue(threads.get() >= 2); - } finally { - executor.shutdownNow(); - } - } - - @Test - public void testAppEngineDelayedCallable() - throws InterruptedException, TimeoutException, ExecutionException { - final AtomicInteger threads = new AtomicInteger(0); - - RevivingScheduledExecutor executor = - new RevivingScheduledExecutor(new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - threads.incrementAndGet(); - return THREAD_FACTORY.newThread(r); - } - }, - "testAppEngineDelayedCallable", - 0, - 100); - - ScheduledFuture future = - executor.schedule( - new Callable() { - @Override - public Boolean call() throws Exception { - return true; - } - }, - 750, - TimeUnit.MILLISECONDS); - - try { - Assert.assertTrue(future.get(1, TimeUnit.SECONDS)); - Assert.assertTrue(threads.get() >= 2); - } finally { - executor.shutdownNow(); - } - } - - @Test - public void testAppEngineCleanup() throws InterruptedException { - final Semaphore beforeSemaphore = new Semaphore(0); - final Semaphore afterSemaphore = new Semaphore(0); - final AtomicInteger threads = new AtomicInteger(0); - - RevivingScheduledExecutor executor = - new RevivingScheduledExecutor(new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - threads.incrementAndGet(); - return THREAD_FACTORY.newThread(r); - } - }, - "testAppEngineCleanup", - 0, - 100) { - @Override - protected void beforeRestart() { - beforeSemaphore.release(); - } - - @Override - protected void afterRestart() { - afterSemaphore.release(); - } - }; - - @SuppressWarnings("unused") - Future possiblyIgnoredError = - executor.submit( - new Runnable() { - @Override - public void run() {} - }); - - try { - Assert.assertTrue(beforeSemaphore.tryAcquire(2, 10, TimeUnit.SECONDS)); - Assert.assertTrue(afterSemaphore.tryAcquire(2, 10, TimeUnit.SECONDS)); - Assert.assertEquals(3, threads.get()); - Assert.assertEquals(0, beforeSemaphore.availablePermits()); - Assert.assertEquals(0, afterSemaphore.availablePermits()); - } finally { - executor.shutdownNow(); - } - } - - private static class ExceptionCatchingThreadFactory implements ThreadFactory { - @Override - public Thread newThread(Runnable r) { - if (r == null) { - return null; - } - Thread thread = Executors.defaultThreadFactory().newThread(r); - thread.setUncaughtExceptionHandler( - new UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread t, Throwable e) { - // ignore -- to prevent the test output from getting cluttered - } - }); - return thread; - } - } -} diff --git a/src/test/java/com/google/firebase/internal/TaskToApiFutureTest.java b/src/test/java/com/google/firebase/internal/TaskToApiFutureTest.java deleted file mode 100644 index e5b64abb3..000000000 --- a/src/test/java/com/google/firebase/internal/TaskToApiFutureTest.java +++ /dev/null @@ -1,81 +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.internal; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import com.google.api.core.ApiFuture; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.firebase.tasks.Task; -import com.google.firebase.tasks.Tasks; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.Test; - -public class TaskToApiFutureTest { - - @Test - public void testGetResult() throws Exception { - Task task = Tasks.forResult("test"); - ApiFuture future = new TaskToApiFuture<>(task); - assertEquals("test", future.get()); - assertFalse(future.isCancelled()); - assertTrue(future.isDone()); - } - - @Test - public void testGetError() throws Exception { - Task task = Tasks.forException(new RuntimeException("test")); - ApiFuture future = new TaskToApiFuture<>(task); - try { - future.get(); - } catch (ExecutionException e) { - assertEquals("test", e.getCause().getMessage()); - } - assertFalse(future.isCancelled()); - assertTrue(future.isDone()); - } - - @Test - public void testAddListener() throws Exception { - Task task = Tasks.forResult("test"); - ApiFuture future = new TaskToApiFuture<>(task); - final AtomicBoolean result = new AtomicBoolean(false); - future.addListener(new Runnable() { - @Override - public void run() { - result.set(true); - } - }, MoreExecutors.directExecutor()); - assertEquals("test", future.get()); - assertTrue(result.get()); - assertFalse(future.isCancelled()); - assertTrue(future.isDone()); - } - - @Test - public void testCancel() throws Exception { - Task task = Tasks.forResult("test"); - ApiFuture future = new TaskToApiFuture<>(task); - assertFalse(future.cancel(true)); - assertEquals("test", future.get()); - assertFalse(future.isCancelled()); - assertTrue(future.isDone()); - } -} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index d31668093..d36db08c0 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -35,6 +35,7 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.testing.GenericFunction; import com.google.firebase.testing.TestResponseInterceptor; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -115,18 +116,34 @@ public void testNullMessage() { public void testSend() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() .setContent(MOCK_RESPONSE); - FirebaseMessaging messaging = initMessaging(response); + final FirebaseMessaging messaging = initMessaging(response); Map> testMessages = buildTestMessages(); - for (Map.Entry> entry : testMessages.entrySet()) { - response.setContent(MOCK_RESPONSE); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); - String resp = messaging.sendAsync(entry.getKey()).get(); - assertEquals("mock-name", resp); - - HttpRequest request = checkRequestHeader(interceptor); - checkRequest(request, ImmutableMap.of("message", entry.getValue())); + List> functions = ImmutableList.of( + new GenericFunction() { + @Override + public String call(Object... args) throws Exception { + return messaging.sendAsync((Message) args[0]).get(); + } + }, + new GenericFunction() { + @Override + public String call(Object... args) throws Exception { + return messaging.send((Message) args[0]); + } + } + ); + for (GenericFunction fn : functions) { + for (Map.Entry> entry : testMessages.entrySet()) { + response.setContent(MOCK_RESPONSE); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + String resp = fn.call(entry.getKey()); + assertEquals("mock-name", resp); + + HttpRequest request = checkRequestHeader(interceptor); + checkRequest(request, ImmutableMap.of("message", entry.getValue())); + } } } @@ -134,18 +151,35 @@ public void testSend() throws Exception { public void testSendDryRun() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() .setContent(MOCK_RESPONSE); - FirebaseMessaging messaging = initMessaging(response); + final FirebaseMessaging messaging = initMessaging(response); Map> testMessages = buildTestMessages(); - for (Map.Entry> entry : testMessages.entrySet()) { - response.setContent(MOCK_RESPONSE); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); - String resp = messaging.sendAsync(entry.getKey(), true).get(); - assertEquals("mock-name", resp); - - HttpRequest request = checkRequestHeader(interceptor); - checkRequest(request, ImmutableMap.of("message", entry.getValue(), "validate_only", true)); + List> functions = ImmutableList.of( + new GenericFunction() { + @Override + public String call(Object... args) throws Exception { + return messaging.sendAsync((Message) args[0], true).get(); + } + }, + new GenericFunction() { + @Override + public String call(Object... args) throws Exception { + return messaging.send((Message) args[0], true); + } + } + ); + + for (GenericFunction fn : functions) { + for (Map.Entry> entry : testMessages.entrySet()) { + response.setContent(MOCK_RESPONSE); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + String resp = fn.call(entry.getKey()); + assertEquals("mock-name", resp); + + HttpRequest request = checkRequestHeader(interceptor); + checkRequest(request, ImmutableMap.of("message", entry.getValue(), "validate_only", true)); + } } } @@ -286,16 +320,41 @@ public void testInvalidSubscribe() { @Test public void testSubscribe() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setContent("{\"results\": [{}, {\"error\": \"error_reason\"}]}"); - FirebaseMessaging messaging = initMessaging(response); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); - - TopicManagementResponse result = messaging.subscribeToTopicAsync( - ImmutableList.of("id1", "id2"), "test-topic").get(); - HttpRequest request = checkTopicManagementRequestHeader(interceptor, TEST_IID_SUBSCRIBE_URL); - checkTopicManagementRequest(request, result); + final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + final FirebaseMessaging messaging = initMessaging(response); + + List> functions = ImmutableList.of( + new GenericFunction() { + @Override + public TopicManagementResponse call(Object... args) throws Exception { + return messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), + "test-topic").get(); + } + }, + new GenericFunction() { + @Override + public TopicManagementResponse call(Object... args) throws Exception { + return messaging.subscribeToTopic(ImmutableList.of("id1", "id2"), "test-topic"); + } + }, + new GenericFunction() { + @Override + public TopicManagementResponse call(Object... args) throws Exception { + return messaging.subscribeToTopic(ImmutableList.of("id1", "id2"), + "/topics/test-topic"); + } + } + ); + + for (GenericFunction fn : functions) { + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + response.setContent(responseString); + TopicManagementResponse result = fn.call(); + HttpRequest request = checkTopicManagementRequestHeader(interceptor, TEST_IID_SUBSCRIBE_URL); + checkTopicManagementRequest(request, result); + } } @Test @@ -341,16 +400,42 @@ public void testInvalidUnsubscribe() { @Test public void testUnsubscribe() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setContent("{\"results\": [{}, {\"error\": \"error_reason\"}]}"); - FirebaseMessaging messaging = initMessaging(response); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); - - TopicManagementResponse result = messaging.unsubscribeFromTopicAsync( - ImmutableList.of("id1", "id2"), "test-topic").get(); - HttpRequest request = checkTopicManagementRequestHeader(interceptor, TEST_IID_UNSUBSCRIBE_URL); - checkTopicManagementRequest(request, result); + final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + final FirebaseMessaging messaging = initMessaging(response); + + List> functions = ImmutableList.of( + new GenericFunction() { + @Override + public TopicManagementResponse call(Object... args) throws Exception { + return messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), + "test-topic").get(); + } + }, + new GenericFunction() { + @Override + public TopicManagementResponse call(Object... args) throws Exception { + return messaging.unsubscribeFromTopic(ImmutableList.of("id1", "id2"), "test-topic"); + } + }, + new GenericFunction() { + @Override + public TopicManagementResponse call(Object... args) throws Exception { + return messaging.unsubscribeFromTopic(ImmutableList.of("id1", "id2"), + "/topics/test-topic"); + } + } + ); + + for (GenericFunction fn : functions) { + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + messaging.setInterceptor(interceptor); + response.setContent(responseString); + TopicManagementResponse result = fn.call(); + HttpRequest request = checkTopicManagementRequestHeader( + interceptor, TEST_IID_UNSUBSCRIBE_URL); + checkTopicManagementRequest(request, result); + } } @Test diff --git a/src/test/java/com/google/firebase/tasks/OnCompleteCompletionListenerTest.java b/src/test/java/com/google/firebase/tasks/OnCompleteCompletionListenerTest.java deleted file mode 100644 index b080558ad..000000000 --- a/src/test/java/com/google/firebase/tasks/OnCompleteCompletionListenerTest.java +++ /dev/null @@ -1,46 +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.tasks; - -import static org.mockito.Mockito.verifyZeroInteractions; - -import com.google.firebase.tasks.testing.TestOnCompleteListener; -import com.google.firebase.testing.MockitoTestRule; -import java.util.concurrent.Executor; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; - -public class OnCompleteCompletionListenerTest { - - @Rule public MockitoTestRule mockitoTestRule = new MockitoTestRule(); - - @Mock private Executor mockExecutor; - - @Test - public void testOnComplete_nothingExecutedAfterCancel() { - OnCompleteCompletionListener listener = - new OnCompleteCompletionListener<>(mockExecutor, new TestOnCompleteListener()); - listener.cancel(); - - TaskImpl task = new TaskImpl<>(); - task.setResult(null); - listener.onComplete(task); - - verifyZeroInteractions(mockExecutor); - } -} diff --git a/src/test/java/com/google/firebase/tasks/OnFailureCompletionListenerTest.java b/src/test/java/com/google/firebase/tasks/OnFailureCompletionListenerTest.java deleted file mode 100644 index 539a062cb..000000000 --- a/src/test/java/com/google/firebase/tasks/OnFailureCompletionListenerTest.java +++ /dev/null @@ -1,59 +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.tasks; - -import static org.mockito.Mockito.verifyZeroInteractions; - -import com.google.firebase.tasks.testing.TestOnFailureListener; -import com.google.firebase.testing.MockitoTestRule; -import java.rmi.RemoteException; -import java.util.concurrent.Executor; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; - -public class OnFailureCompletionListenerTest { - - @Rule public MockitoTestRule mockitoTestRule = new MockitoTestRule(); - - @Mock private Executor mockExecutor; - - @Test - public void testOnComplete_nothingExecutedAfterCancel() { - OnFailureCompletionListener listener = - new OnFailureCompletionListener<>(mockExecutor, new TestOnFailureListener()); - listener.cancel(); - - TaskImpl task = new TaskImpl<>(); - task.setException(new RemoteException()); - listener.onComplete(task); - - verifyZeroInteractions(mockExecutor); - } - - @Test - public void testOnComplete_nothingExecutedOnSuccess() { - OnFailureCompletionListener listener = - new OnFailureCompletionListener<>(mockExecutor, new TestOnFailureListener()); - - TaskImpl task = new TaskImpl<>(); - task.setResult(null); - listener.onComplete(task); - - verifyZeroInteractions(mockExecutor); - } -} diff --git a/src/test/java/com/google/firebase/tasks/OnSuccessCompletionListenerTest.java b/src/test/java/com/google/firebase/tasks/OnSuccessCompletionListenerTest.java deleted file mode 100644 index 0fa73b1b9..000000000 --- a/src/test/java/com/google/firebase/tasks/OnSuccessCompletionListenerTest.java +++ /dev/null @@ -1,59 +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.tasks; - -import static org.mockito.Mockito.verifyZeroInteractions; - -import com.google.firebase.tasks.testing.TestOnSuccessListener; -import com.google.firebase.testing.MockitoTestRule; -import java.rmi.RemoteException; -import java.util.concurrent.Executor; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; - -public class OnSuccessCompletionListenerTest { - - @Rule public MockitoTestRule mockitoTestRule = new MockitoTestRule(); - - @Mock private Executor mockExecutor; - - @Test - public void testOnComplete_nothingExecutedAfterCancel() { - OnSuccessCompletionListener listener = - new OnSuccessCompletionListener<>(mockExecutor, new TestOnSuccessListener<>()); - listener.cancel(); - - TaskImpl task = new TaskImpl<>(); - task.setResult(null); - listener.onComplete(task); - - verifyZeroInteractions(mockExecutor); - } - - @Test - public void testOnComplete_nothingExecutedOnFailure() { - OnSuccessCompletionListener listener = - new OnSuccessCompletionListener<>(mockExecutor, new TestOnSuccessListener<>()); - - TaskImpl task = new TaskImpl<>(); - task.setException(new RemoteException()); - listener.onComplete(task); - - verifyZeroInteractions(mockExecutor); - } -} diff --git a/src/test/java/com/google/firebase/tasks/TaskCompletionSourceTest.java b/src/test/java/com/google/firebase/tasks/TaskCompletionSourceTest.java deleted file mode 100644 index 54e4be36e..000000000 --- a/src/test/java/com/google/firebase/tasks/TaskCompletionSourceTest.java +++ /dev/null @@ -1,103 +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.tasks; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import java.rmi.RemoteException; -import org.junit.Test; - -public class TaskCompletionSourceTest { - - private static final String RESULT = "Success"; - private static final String RESULT_2 = "Success2"; - private static final RemoteException EXCEPTION = new RemoteException(); - private static final RemoteException EXCEPTION_2 = new RemoteException("2"); - - @Test - public void testSetResult() throws Exception { - TaskCompletionSource source = new TaskCompletionSource<>(); - Task task = source.getTask(); - - assertNotNull(task); - assertFalse(task.isComplete()); - - source.setResult(RESULT); - - assertTrue(task.isComplete()); - assertTrue(task.isSuccessful()); - assertEquals(RESULT, task.getResult()); - } - - @Test - public void testTrySetResult() throws Exception { - TaskCompletionSource source = new TaskCompletionSource<>(); - Task task = source.getTask(); - - assertTrue(source.trySetResult(RESULT)); - assertEquals(RESULT, task.getResult()); - } - - @Test - public void testTrySetResult_alreadySet() throws Exception { - TaskCompletionSource source = new TaskCompletionSource<>(); - Task task = source.getTask(); - - source.setResult(RESULT); - // Expect no exception here. - assertFalse(source.trySetResult(RESULT_2)); - assertEquals(RESULT, task.getResult()); - } - - @Test - public void testSetException() { - TaskCompletionSource source = new TaskCompletionSource<>(); - Task task = source.getTask(); - - assertNotNull(task); - assertFalse(task.isComplete()); - - source.setException(EXCEPTION); - - assertTrue(task.isComplete()); - assertFalse(task.isSuccessful()); - assertEquals(EXCEPTION, task.getException()); - } - - @Test - public void testTrySetException() { - TaskCompletionSource source = new TaskCompletionSource<>(); - Task task = source.getTask(); - - assertTrue(source.trySetException(EXCEPTION)); - assertEquals(EXCEPTION, task.getException()); - } - - @Test - public void testTrySetException_alreadySet() { - TaskCompletionSource source = new TaskCompletionSource<>(); - Task task = source.getTask(); - - source.setException(EXCEPTION); - // Expect no exception here. - assertFalse(source.trySetException(EXCEPTION_2)); - assertEquals(EXCEPTION, task.getException()); - } -} diff --git a/src/test/java/com/google/firebase/tasks/TaskExecutorsTest.java b/src/test/java/com/google/firebase/tasks/TaskExecutorsTest.java deleted file mode 100644 index 57e7f04b2..000000000 --- a/src/test/java/com/google/firebase/tasks/TaskExecutorsTest.java +++ /dev/null @@ -1,55 +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.tasks; - -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.TimeUnit; -import org.junit.Assert; -import org.junit.Test; - -public class TaskExecutorsTest { - - private static final long TIMEOUT_MS = 500; - - @Test - public void testDefaultThreadPool() throws InterruptedException { - final ArrayBlockingQueue sync = new ArrayBlockingQueue<>(1); - TaskExecutors.DEFAULT_THREAD_POOL.execute( - new Runnable() { - @Override - public void run() { - sync.add(Thread.currentThread()); - } - }); - Thread actual = sync.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS); - Assert.assertNotEquals(Thread.currentThread(), actual); - } - - @Test - public void testDirect() throws InterruptedException { - final ArrayBlockingQueue sync = new ArrayBlockingQueue<>(1); - TaskExecutors.DIRECT.execute( - new Runnable() { - @Override - public void run() { - sync.add(Thread.currentThread()); - } - }); - Thread actual = sync.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS); - Assert.assertEquals(Thread.currentThread(), actual); - } -} diff --git a/src/test/java/com/google/firebase/tasks/TaskImplTest.java b/src/test/java/com/google/firebase/tasks/TaskImplTest.java deleted file mode 100644 index 8a3a65c25..000000000 --- a/src/test/java/com/google/firebase/tasks/TaskImplTest.java +++ /dev/null @@ -1,600 +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.tasks; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -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.firebase.internal.NonNull; -import com.google.firebase.tasks.testing.TestOnCompleteListener; -import com.google.firebase.tasks.testing.TestOnFailureListener; -import com.google.firebase.tasks.testing.TestOnSuccessListener; -import java.rmi.RemoteException; -import org.junit.Test; - -public class TaskImplTest { - - private static final Exception EXCEPTION = new RemoteException(); - private static final Void NULL_RESULT = null; - private static final String NON_NULL_RESULT = "Success"; - - @Test - public void testIsComplete_notComplete() { - TaskImpl task = new TaskImpl<>(); - assertFalse(task.isComplete()); - } - - @Test - public void testIsComplete_failed() { - TaskImpl task = new TaskImpl<>(); - task.setException(EXCEPTION); - assertTrue(task.isComplete()); - } - - @Test - public void testIsComplete_nullResult() { - TaskImpl task = new TaskImpl<>(); - task.setResult(NULL_RESULT); - assertTrue(task.isComplete()); - } - - @Test - public void testIsComplete_nonNullResult() { - TaskImpl task = new TaskImpl<>(); - task.setResult(NON_NULL_RESULT); - assertTrue(task.isComplete()); - } - - @Test - public void testIsSuccessful_notComplete() { - TaskImpl task = new TaskImpl<>(); - assertFalse(task.isSuccessful()); - } - - @Test - public void testIsSuccessful_failed() { - TaskImpl task = new TaskImpl<>(); - task.setException(EXCEPTION); - assertFalse(task.isSuccessful()); - } - - @Test - public void testIsSuccessful_nullResult() { - TaskImpl task = new TaskImpl<>(); - task.setResult(NULL_RESULT); - assertTrue(task.isSuccessful()); - } - - @Test - public void testIsSuccessful_nonNullResult() { - TaskImpl task = new TaskImpl<>(); - task.setResult(NON_NULL_RESULT); - assertTrue(task.isSuccessful()); - } - - @Test(expected = IllegalStateException.class) - public void testGetResult_notComplete() { - TaskImpl task = new TaskImpl<>(); - task.getResult(); - } - - @Test - public void testGetResult_nullResult() { - TaskImpl task = new TaskImpl<>(); - task.setResult(NULL_RESULT); - assertNull(task.getResult()); - } - - @Test - public void testGetResult_nonNullResult() { - TaskImpl task = new TaskImpl<>(); - task.setResult(NON_NULL_RESULT); - assertEquals(NON_NULL_RESULT, task.getResult()); - } - - @Test - public void testTrySetResult_nullResult() { - TaskImpl task = new TaskImpl<>(); - assertTrue(task.trySetResult(NULL_RESULT)); - assertNull(task.getResult()); - } - - @Test - public void testTrySetResult_nonNullResult() { - TaskImpl task = new TaskImpl<>(); - assertTrue(task.trySetResult(NON_NULL_RESULT)); - assertEquals(NON_NULL_RESULT, task.getResult()); - } - - @Test - public void testGetResult_failure() { - TaskImpl task = new TaskImpl<>(); - task.setException(EXCEPTION); - - try { - task.getResult(); - fail("No exception thrown"); - } catch (RuntimeExecutionException e) { - assertSame(EXCEPTION, e.getCause()); - } - } - - @Test - public void testGetResult_exceptionIsSpecifiedType() throws Exception { - TaskImpl task = new TaskImpl<>(); - task.setException(EXCEPTION); - - try { - task.getResult(RemoteException.class); - fail("No exception thrown"); - } catch (RemoteException e) { - assertSame(EXCEPTION, e); - } - } - - @Test - public void testGetResult_exceptionIsNotSpecifiedType() throws Exception { - TaskImpl task = new TaskImpl<>(); - Exception exception = new RuntimeException(); - task.setException(exception); - - try { - task.getResult(RemoteException.class); - fail("No exception thrown"); - } catch (RuntimeExecutionException e) { - assertSame(exception, e.getCause()); - } - } - - @Test - public void testGetException_notComplete() { - TaskImpl task = new TaskImpl<>(); - assertNull(task.getException()); - } - - @Test - public void testGetException_failure() { - TaskImpl task = new TaskImpl<>(); - task.setException(EXCEPTION); - assertEquals(EXCEPTION, task.getException()); - } - - @Test - public void testTrySetException() { - TaskImpl task = new TaskImpl<>(); - assertTrue(task.trySetException(EXCEPTION)); - assertEquals(EXCEPTION, task.getException()); - } - - @Test - public void testGetException_nullResult() { - TaskImpl task = new TaskImpl<>(); - task.setResult(NULL_RESULT); - assertNull(task.getException()); - } - - @Test - public void testGetException_nonNullResult() { - TaskImpl task = new TaskImpl<>(); - task.setResult(NON_NULL_RESULT); - assertNull(task.getException()); - } - - @Test - public void testOnSuccess_nullResult() throws Exception { - TaskImpl task = new TaskImpl<>(); - TestOnSuccessListener listener = new TestOnSuccessListener<>(); - task.addOnSuccessListener(listener); - - task.setResult(NULL_RESULT); - - assertTrue(listener.await()); - assertNull(listener.getResult()); - } - - @Test - public void testOnSuccess_nonNullResult() throws Exception { - TaskImpl task = new TaskImpl<>(); - TestOnSuccessListener listener = new TestOnSuccessListener<>(); - task.addOnSuccessListener(listener); - - task.setResult(NON_NULL_RESULT); - - assertTrue(listener.await()); - assertEquals(NON_NULL_RESULT, listener.getResult()); - } - - @Test - public void testOnSuccess_alreadyComplete() throws Exception { - TaskImpl task = new TaskImpl<>(); - TestOnSuccessListener listener = new TestOnSuccessListener<>(); - task.setResult(NON_NULL_RESULT); - - task.addOnSuccessListener(listener); - - assertTrue(listener.await()); - assertEquals(NON_NULL_RESULT, listener.getResult()); - } - - @Test - public void testOnSuccess_acceptsBaseResult() throws Exception { - TaskImpl task = new TaskImpl<>(); - TestOnSuccessListener listener = new TestOnSuccessListener<>(); - task.setResult(NON_NULL_RESULT); - - task.addOnSuccessListener(listener); - - assertTrue(listener.await()); - assertEquals(NON_NULL_RESULT, listener.getResult()); - } - - @Test - public void testOnFailure() throws Exception { - TaskImpl task = new TaskImpl<>(); - TestOnFailureListener listener = new TestOnFailureListener(); - task.addOnFailureListener(listener); - - task.setException(EXCEPTION); - - assertTrue(listener.await()); - assertEquals(EXCEPTION, listener.getException()); - } - - @Test - public void testOnFailure_alreadyComplete() throws Exception { - TaskImpl task = new TaskImpl<>(); - TestOnFailureListener listener = new TestOnFailureListener(); - task.setException(EXCEPTION); - - task.addOnFailureListener(listener); - - assertTrue(listener.await()); - assertEquals(EXCEPTION, listener.getException()); - } - - @Test - public void testOnComplete_nullResult() throws Exception { - TaskImpl task = new TaskImpl<>(); - TestOnCompleteListener listener = new TestOnCompleteListener<>(); - task.addOnCompleteListener(listener); - - task.setResult(NULL_RESULT); - - assertTrue(listener.await()); - assertEquals(task, listener.getTask()); - } - - @Test - public void testOnComplete_nonNullResult() throws Exception { - TaskImpl task = new TaskImpl<>(); - TestOnCompleteListener listener = new TestOnCompleteListener<>(); - task.addOnCompleteListener(listener); - - task.setResult(NON_NULL_RESULT); - - assertTrue(listener.await()); - assertEquals(task, listener.getTask()); - } - - @Test - public void testOnComplete_failure() throws Exception { - TaskImpl task = new TaskImpl<>(); - TestOnCompleteListener listener = new TestOnCompleteListener<>(); - task.addOnCompleteListener(listener); - - task.setException(EXCEPTION); - - assertTrue(listener.await()); - assertEquals(task, listener.getTask()); - } - - @Test - public void testOnComplete_alreadySucceeded() throws Exception { - TaskImpl task = new TaskImpl<>(); - task.setResult(NULL_RESULT); - - TestOnCompleteListener listener = new TestOnCompleteListener<>(); - task.addOnCompleteListener(listener); - - assertTrue(listener.await()); - assertEquals(task, listener.getTask()); - } - - @Test - public void testOnComplete_alreadyFailed() throws Exception { - TaskImpl task = new TaskImpl<>(); - task.setException(EXCEPTION); - - TestOnCompleteListener listener = new TestOnCompleteListener<>(); - task.addOnCompleteListener(listener); - - assertTrue(listener.await()); - assertEquals(task, listener.getTask()); - } - - @Test - public void testContinueWith() { - TaskImpl task = new TaskImpl<>(); - Task task2 = - task.continueWith( - TaskExecutors.DIRECT, - new Continuation() { - @Override - public String then(@NonNull Task task) throws Exception { - assertNull(task.getResult()); - return NON_NULL_RESULT; - } - }); - task.setResult(null); - assertEquals(NON_NULL_RESULT, task2.getResult()); - } - - @Test - public void testContinueWith_alreadyComplete() { - TaskImpl task = new TaskImpl<>(); - task.setResult(null); - Task task2 = - task.continueWith( - TaskExecutors.DIRECT, - new Continuation() { - @Override - public Object then(@NonNull Task task) throws Exception { - assertNull(task.getResult()); - return NON_NULL_RESULT; - } - }); - assertEquals(NON_NULL_RESULT, task2.getResult()); - } - - @Test - public void testContinueWith_propagatesException() { - TaskImpl task = new TaskImpl<>(); - Task task2 = - task.continueWith( - TaskExecutors.DIRECT, - new Continuation() { - @Override - public Void then(@NonNull Task task) throws Exception { - task.getResult(); - throw new AssertionError("Expected getResult to throw"); - } - }); - task.setException(EXCEPTION); - assertEquals(EXCEPTION, task2.getException()); - } - - @Test - public void testContinueWith_continuationThrows() { - TaskImpl task = new TaskImpl<>(); - Task task2 = - task.continueWith( - TaskExecutors.DIRECT, - new Continuation() { - @Override - public Void then(@NonNull Task task) throws Exception { - throw EXCEPTION; - } - }); - task.setResult(null); - assertEquals(EXCEPTION, task2.getException()); - } - - @Test - public void testContinueWith_continuationThrowsWrapperWithoutCause() { - TaskImpl task = new TaskImpl<>(); - Task task2 = - task.continueWith( - TaskExecutors.DIRECT, - new Continuation() { - @Override - public Void then(@NonNull Task task) throws Exception { - throw new RuntimeExecutionException(null); - } - }); - task.setResult(null); - assertTrue(task2.getException() instanceof RuntimeExecutionException); - } - - @Test - public void testContinueWith_continuationReturnsNull() { - TaskImpl task = new TaskImpl<>(); - Task task2 = - task.continueWith( - TaskExecutors.DIRECT, - new Continuation() { - @Override - public Void then(@NonNull Task task) throws Exception { - assertEquals(NON_NULL_RESULT, task.getResult()); - return null; - } - }); - task.setResult(NON_NULL_RESULT); - assertNull(task2.getResult()); - } - - @Test - public void testContinueWithTask() { - TaskImpl task = new TaskImpl<>(); - Task task2 = - task.continueWithTask( - TaskExecutors.DIRECT, - new Continuation>() { - @Override - public Task then(@NonNull Task task) throws Exception { - assertNull(task.getResult()); - return Tasks.forResult(NON_NULL_RESULT); - } - }); - task.setResult(null); - assertEquals(NON_NULL_RESULT, task2.getResult()); - } - - @Test - public void testContinueWithTask_alreadyComplete() { - TaskImpl task = new TaskImpl<>(); - task.setResult(null); - Task task2 = - task.continueWithTask( - TaskExecutors.DIRECT, - new Continuation>() { - @Override - public Task then(@NonNull Task task) throws Exception { - assertNull(task.getResult()); - return Tasks.forResult(NON_NULL_RESULT); - } - }); - assertEquals(NON_NULL_RESULT, task2.getResult()); - } - - @Test - public void testContinueWithTask_propagatesException() { - TaskImpl task = new TaskImpl<>(); - Task task2 = - task.continueWithTask( - TaskExecutors.DIRECT, - new Continuation>() { - @Override - public Task then(@NonNull Task task) throws Exception { - task.getResult(); - throw new AssertionError("Expected getResult to throw"); - } - }); - task.setException(EXCEPTION); - assertEquals(EXCEPTION, task2.getException()); - } - - @Test - public void testContinueWithTask_continuationThrows() { - TaskImpl task = new TaskImpl<>(); - Task task2 = - task.continueWithTask( - TaskExecutors.DIRECT, - new Continuation>() { - @Override - public Task then(@NonNull Task task) throws Exception { - throw EXCEPTION; - } - }); - task.setResult(null); - assertEquals(EXCEPTION, task2.getException()); - } - - @Test - public void testContinueWithTask_continuationThrowsWrapperWithoutCause() { - TaskImpl task = new TaskImpl<>(); - Task task2 = - task.continueWithTask( - TaskExecutors.DIRECT, - new Continuation>() { - @Override - public Task then(@NonNull Task task) throws Exception { - throw new RuntimeExecutionException(null); - } - }); - task.setResult(null); - assertTrue(task2.getException() instanceof RuntimeExecutionException); - } - - @Test - public void testContinueWithTask_continuationReturnsIncompleteTask() { - TaskImpl task = new TaskImpl<>(); - final TaskImpl task2 = new TaskImpl<>(); - Task task3 = - task.continueWithTask( - TaskExecutors.DIRECT, - new Continuation>() { - @Override - public Task then(@NonNull Task task) throws Exception { - return task2; - } - }); - task.setResult(NULL_RESULT); - assertFalse(task3.isComplete()); - - task2.setResult(NON_NULL_RESULT); - assertEquals(NON_NULL_RESULT, task3.getResult()); - } - - @Test - public void testContinueWithTask_continuationReturnsOriginalTask() { - TaskImpl task = new TaskImpl<>(); - Task task2 = - task.continueWithTask( - TaskExecutors.DIRECT, - new Continuation>() { - @Override - public Task then(@NonNull Task task) throws Exception { - return task; - } - }); - task.setResult(NON_NULL_RESULT); - assertEquals(NON_NULL_RESULT, task2.getResult()); - } - - @Test - public void testContinueWithTask_continuationReturnsNull() { - TaskImpl task = new TaskImpl<>(); - Task task2 = - task.continueWithTask( - TaskExecutors.DIRECT, - new Continuation>() { - @Override - public Task then(@NonNull Task task) throws Exception { - assertEquals(NON_NULL_RESULT, task.getResult()); - return null; - } - }); - task.setResult(NON_NULL_RESULT); - assertTrue(task2.getException() instanceof NullPointerException); - } - - @Test(expected = IllegalStateException.class) - public void testSetResult_alreadyComplete() { - TaskImpl task = new TaskImpl<>(); - task.setException(EXCEPTION); - task.setResult(NULL_RESULT); - } - - @Test - public void testTrySetResult_alreadyComplete() { - TaskImpl task = new TaskImpl<>(); - task.setException(EXCEPTION); - // Expect no exception to be thrown. - assertFalse(task.trySetResult(NULL_RESULT)); - assertEquals(EXCEPTION, task.getException()); - } - - @Test(expected = IllegalStateException.class) - public void testSetException_alreadyComplete() { - TaskImpl task = new TaskImpl<>(); - task.setResult(NULL_RESULT); - task.setException(EXCEPTION); - } - - @Test - public void testTrySetException_alreadyComplete() { - TaskImpl task = new TaskImpl<>(); - task.setResult(NULL_RESULT); - // Expect no exception to be thrown. - assertFalse(task.trySetException(EXCEPTION)); - assertNull(task.getResult()); - } -} diff --git a/src/test/java/com/google/firebase/tasks/TasksTest.java b/src/test/java/com/google/firebase/tasks/TasksTest.java deleted file mode 100644 index 067ed93c2..000000000 --- a/src/test/java/com/google/firebase/tasks/TasksTest.java +++ /dev/null @@ -1,315 +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.tasks; - -import static org.junit.Assert.assertEquals; -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 java.rmi.RemoteException; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import org.junit.Test; - -public class TasksTest { - - private static final Object RESULT = new Object(); - private static final RemoteException EXCEPTION = new RemoteException(); - private static final int SCHEDULE_DELAY_MS = 50; - private static final int TIMEOUT_MS = 200; - private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); - - @Test - public void testForResult() throws Exception { - Task task = Tasks.forResult(RESULT); - assertEquals(RESULT, task.getResult()); - } - - @Test - public void testForResult_nullResult() throws Exception { - Task task = Tasks.forResult(null); - assertNull(task.getResult()); - } - - @Test - public void testForException() { - Task task = Tasks.forException(EXCEPTION); - assertEquals(EXCEPTION, task.getException()); - } - - @Test(expected = NullPointerException.class) - @SuppressWarnings("ConstantConditions") - public void testForException_nullException() { - Tasks.forException(null); - } - - @Test - public void testCall_nonNullResult() { - Task task = - Tasks.call( - TaskExecutors.DIRECT, - new Callable() { - @Override - public Object call() throws Exception { - return RESULT; - } - }); - assertEquals(RESULT, task.getResult()); - } - - @Test - public void testCall_nullResult() { - Task task = - Tasks.call( - TaskExecutors.DIRECT, - new Callable() { - @Override - public Void call() throws Exception { - return null; - } - }); - assertNull(task.getResult()); - } - - @Test - public void testCall_exception() { - Task task = - Tasks.call( - TaskExecutors.DIRECT, - new Callable() { - @Override - public Void call() throws Exception { - throw EXCEPTION; - } - }); - assertEquals(EXCEPTION, task.getException()); - } - - @Test(expected = NullPointerException.class) - public void testCall_nullCallable() { - Tasks.call(null); - } - - @Test(expected = NullPointerException.class) - public void testCall_nullExecutor() { - Tasks.call( - null, - new Callable() { - @Override - public Void call() throws Exception { - return null; - } - }); - } - - @Test - public void testAwait() throws Exception { - TaskCompletionSource completionSource = new TaskCompletionSource<>(); - scheduleResult(completionSource); - assertEquals( - RESULT, Tasks.await(completionSource.getTask(), TIMEOUT_MS, TimeUnit.MILLISECONDS)); - } - - @Test - public void testAwait_noTimeout() throws Exception { - TaskCompletionSource completionSource = new TaskCompletionSource<>(); - scheduleResult(completionSource); - assertEquals(RESULT, Tasks.await(completionSource.getTask())); - } - - @Test - public void testAwait_exception() throws Exception { - TaskCompletionSource completionSource = new TaskCompletionSource<>(); - scheduleException(completionSource); - - try { - Tasks.await(completionSource.getTask(), TIMEOUT_MS, TimeUnit.MILLISECONDS); - fail("No exception thrown"); - } catch (ExecutionException e) { - assertSame(EXCEPTION, e.getCause()); - } - } - - @Test - public void testAwait_noTimeoutException() throws Exception { - TaskCompletionSource completionSource = new TaskCompletionSource<>(); - scheduleException(completionSource); - - try { - Tasks.await(completionSource.getTask()); - fail("No exception thrown"); - } catch (ExecutionException e) { - assertSame(EXCEPTION, e.getCause()); - } - } - - @Test - public void testAwait_alreadyFailed() throws Exception { - Task task = Tasks.forException(EXCEPTION); - - try { - Tasks.await(task, TIMEOUT_MS, TimeUnit.MILLISECONDS); - fail("No exception thrown"); - } catch (ExecutionException e) { - assertSame(EXCEPTION, e.getCause()); - } - } - - @Test - public void testAwait_noTimeoutAlreadyFailed() throws Exception { - Task task = Tasks.forException(EXCEPTION); - - try { - Tasks.await(task); - fail("No exception thrown"); - } catch (ExecutionException e) { - assertSame(EXCEPTION, e.getCause()); - } - } - - @Test - public void testAwait_alreadySucceeded() throws Exception { - Task task = Tasks.forResult(RESULT); - assertEquals(RESULT, Tasks.await(task, TIMEOUT_MS, TimeUnit.MILLISECONDS)); - } - - @Test - public void testAwait_noTimeoutAlreadySucceeded() throws Exception { - Task task = Tasks.forResult(RESULT); - assertEquals(RESULT, Tasks.await(task)); - } - - @Test(expected = InterruptedException.class) - public void testAwait_interrupted() throws Exception { - Task task = new TaskImpl<>(); - scheduleInterrupt(); - Tasks.await(task, TIMEOUT_MS, TimeUnit.MILLISECONDS); - } - - @Test(expected = InterruptedException.class) - public void testAwait_noTimeoutInterrupted() throws Exception { - Task task = new TaskImpl<>(); - scheduleInterrupt(); - Tasks.await(task); - } - - @Test(expected = TimeoutException.class) - public void testAwait_timeout() throws Exception { - TaskImpl task = new TaskImpl<>(); - Tasks.await(task, TIMEOUT_MS, TimeUnit.MILLISECONDS); - } - - @Test(expected = TimeoutException.class) - public void testWhenAll_notCompleted() throws Exception { - Task task1 = new TaskImpl<>(); - Task task2 = new TaskImpl<>(); - Task task = Tasks.whenAll(task1, task2); - Tasks.await(task, TIMEOUT_MS, TimeUnit.MILLISECONDS); - } - - @Test(expected = TimeoutException.class) - public void testWhenAll_partiallyCompleted() throws Exception { - Task task1 = Tasks.forResult(RESULT); - Task task2 = new TaskImpl<>(); - Task task = Tasks.whenAll(task1, task2); - Tasks.await(task, TIMEOUT_MS, TimeUnit.MILLISECONDS); - } - - @Test - public void testWhenAll_completedFailure() throws Exception { - Task task1 = Tasks.forResult(RESULT); - Task task2 = Tasks.forException(EXCEPTION); - Task task = Tasks.whenAll(task1, task2); - - try { - Tasks.await(task); - fail("No exception thrown"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof ExecutionException); - } - } - - @Test - public void testWhenAll_completedSuccess() throws Exception { - Task task1 = Tasks.forResult(RESULT); - Task task2 = Tasks.forResult(RESULT); - Task task = Tasks.whenAll(task1, task2); - assertNull(Tasks.await(task)); - } - - @Test - public void testWhenAll_completedEmpty() throws Exception { - Task task = Tasks.whenAll(); - assertNull(Tasks.await(task)); - } - - @Test(expected = NullPointerException.class) - public void testWhenAll_nullOnInput() throws Exception { - Task task = Tasks.forResult(RESULT); - Tasks.whenAll(task, null, task); - } - - private void scheduleResult(final TaskCompletionSource completionSource) { - @SuppressWarnings("unused") - Future possiblyIgnoredError = - executor.schedule( - new Runnable() { - @Override - public void run() { - completionSource.setResult(RESULT); - } - }, - SCHEDULE_DELAY_MS, - TimeUnit.MILLISECONDS); - } - - private void scheduleException(final TaskCompletionSource completionSource) { - @SuppressWarnings("unused") - Future possiblyIgnoredError = - executor.schedule( - new Runnable() { - @Override - public void run() { - completionSource.setException(EXCEPTION); - } - }, - SCHEDULE_DELAY_MS, - TimeUnit.MILLISECONDS); - } - - private void scheduleInterrupt() { - final Thread testThread = Thread.currentThread(); - @SuppressWarnings("unused") - Future possiblyIgnoredError = - executor.schedule( - new Runnable() { - @Override - public void run() { - testThread.interrupt(); - } - }, - SCHEDULE_DELAY_MS, - TimeUnit.MILLISECONDS); - } -} diff --git a/src/test/java/com/google/firebase/tasks/testing/TestOnCompleteListener.java b/src/test/java/com/google/firebase/tasks/testing/TestOnCompleteListener.java deleted file mode 100644 index 3466597b4..000000000 --- a/src/test/java/com/google/firebase/tasks/testing/TestOnCompleteListener.java +++ /dev/null @@ -1,67 +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.tasks.testing; - -import static com.google.common.base.Preconditions.checkState; - -import com.google.firebase.internal.NonNull; -import com.google.firebase.tasks.OnCompleteListener; -import com.google.firebase.tasks.Task; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * Implementation of {@link OnCompleteListener} for use in tests. - */ -public class TestOnCompleteListener implements OnCompleteListener { - - private static final long TIMEOUT_MS = 500; - - private final CountDownLatch latch = new CountDownLatch(1); - private Task task; - private Thread thread; - - @Override - public void onComplete(@NonNull Task task) { - this.task = task; - thread = Thread.currentThread(); - latch.countDown(); - } - - /** - * Blocks until the {@link #onComplete} is called. - */ - public boolean await() throws InterruptedException { - return latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS); - } - - /** - * Returns the Task passed to {@link #onComplete}. - */ - public Task getTask() { - checkState(latch.getCount() == 0, "onComplete has not been called"); - return task; - } - - /** - * Returns the Thread that {@link #onComplete} was called on. - */ - public Thread getThread() { - checkState(latch.getCount() == 0, "onFailure has not been called"); - return thread; - } -} diff --git a/src/test/java/com/google/firebase/tasks/testing/TestOnFailureListener.java b/src/test/java/com/google/firebase/tasks/testing/TestOnFailureListener.java deleted file mode 100644 index ba436a0c7..000000000 --- a/src/test/java/com/google/firebase/tasks/testing/TestOnFailureListener.java +++ /dev/null @@ -1,66 +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.tasks.testing; - -import static com.google.common.base.Preconditions.checkState; - -import com.google.firebase.internal.NonNull; -import com.google.firebase.tasks.OnFailureListener; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * Implementation of {@link OnFailureListener} for use in tests. - */ -public class TestOnFailureListener implements OnFailureListener { - - private static final long TIMEOUT_MS = 500; - - private final CountDownLatch latch = new CountDownLatch(1); - private Exception exception; - private Thread thread; - - @Override - public void onFailure(@NonNull Exception e) { - exception = e; - thread = Thread.currentThread(); - latch.countDown(); - } - - /** - * Blocks until the {@link #onFailure} is called. - */ - public boolean await() throws InterruptedException { - return latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS); - } - - /** - * Returns the exception passed to {@link #onFailure}. - */ - public Exception getException() { - checkState(latch.getCount() == 0, "onFailure has not been called"); - return exception; - } - - /** - * Returns the Thread that {@link #onFailure} was called on. - */ - public Thread getThread() { - checkState(latch.getCount() == 0, "onFailure has not been called"); - return thread; - } -} diff --git a/src/test/java/com/google/firebase/tasks/testing/TestOnSuccessListener.java b/src/test/java/com/google/firebase/tasks/testing/TestOnSuccessListener.java deleted file mode 100644 index 71ba3ffb9..000000000 --- a/src/test/java/com/google/firebase/tasks/testing/TestOnSuccessListener.java +++ /dev/null @@ -1,65 +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.tasks.testing; - -import static com.google.common.base.Preconditions.checkState; - -import com.google.firebase.tasks.OnSuccessListener; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * Implementation of {@link OnSuccessListener} for use in tests. - */ -public class TestOnSuccessListener implements OnSuccessListener { - - private static final long TIMEOUT_MS = 500; - - private final CountDownLatch latch = new CountDownLatch(1); - private T result; - private Thread thread; - - @Override - public void onSuccess(T result) { - this.result = result; - this.thread = Thread.currentThread(); - latch.countDown(); - } - - /** - * Blocks until the {@link #onSuccess} is called. - */ - public boolean await() throws InterruptedException { - return latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS); - } - - /** - * Returns the result passed to {@link #onSuccess}. - */ - public T getResult() { - checkState(latch.getCount() == 0, "onSuccess has not been called"); - return result; - } - - /** - * Returns the Thread that {@link #onSuccess} was called on. - */ - public Thread getThread() { - checkState(latch.getCount() == 0, "onSuccess has not been called"); - return thread; - } -} diff --git a/src/main/java/com/google/firebase/database/logging/package-info.java b/src/test/java/com/google/firebase/testing/GenericFunction.java similarity index 75% rename from src/main/java/com/google/firebase/database/logging/package-info.java rename to src/test/java/com/google/firebase/testing/GenericFunction.java index 37bc5cf37..b7dc8a84a 100644 --- a/src/main/java/com/google/firebase/database/logging/package-info.java +++ b/src/test/java/com/google/firebase/testing/GenericFunction.java @@ -14,7 +14,13 @@ * limitations under the License. */ +package com.google.firebase.testing; + /** - * @hide + * A generic operation that can be called with arbitrary arguments. */ -package com.google.firebase.database.logging; +public interface GenericFunction { + + T call(Object ...args) throws Exception; + +} From 5231f0b7f176709dd637044cf93c37eb9f3f7d71 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 8 May 2018 10:08:07 -0700 Subject: [PATCH 011/456] Staged Release 6.0.0 (#172) * Updating CHANGELOG for 6.0.0 release. * [maven-release-plugin] prepare release v6.0.0 * [maven-release-plugin] prepare for next development iteration * Removing extra whitespacw --- CHANGELOG.md | 4 ++++ pom.xml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eba2e178f..14f28a804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +- + +# v6.0.0 + - [added] `FirebaseAuth`, `FirebaseMessaging` and `FirebaseInstanceId` interfaces now expose a set of blocking methods. Each operation has blocking an asynchronous versions. diff --git a/pom.xml b/pom.xml index 7b15a89ad..8e2190796 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 5.11.1-SNAPSHOT + 6.0.1-SNAPSHOT jar firebase-admin From 04e2b340292e56e296082491e2066f77aa6e3880 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 11 May 2018 13:28:56 -0700 Subject: [PATCH 012/456] Updating snippets to use the blocking APIs (#175) --- .../google/firebase/auth/FirebaseAuth.java | 12 +- .../snippets/FirebaseAuthSnippets.java | 124 ++++++++---------- .../snippets/FirebaseMessagingSnippets.java | 29 ++-- 3 files changed, 83 insertions(+), 82 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 3181dc5b1..ceaf33485 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -782,13 +782,21 @@ protected UserRecord execute() throws FirebaseAuthException { * @throws IllegalArgumentException If the user ID string is null or empty, or the claims * payload is invalid or too large. */ - public void setCustomClaims(@NonNull String uid, + public void setCustomUserClaims(@NonNull String uid, @Nullable Map claims) throws FirebaseAuthException { setCustomUserClaimsOp(uid, claims).call(); } /** - * Similar to {@link #setCustomClaims(String, Map)} but performs the operation asynchronously. + * @deprecated Use {@link #setCustomUserClaims(String, Map)} instead. + */ + public void setCustomClaims(@NonNull String uid, + @Nullable Map claims) throws FirebaseAuthException { + setCustomUserClaims(uid, claims); + } + + /** + * Similar to {@link #setCustomUserClaims(String, Map)} but performs the operation asynchronously. * * @param uid A user ID string. * @param claims A map of custom claims or null. diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java index ed219bd86..52c78de8c 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java @@ -30,7 +30,6 @@ import java.net.URI; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import javax.ws.rs.Consumes; import javax.ws.rs.CookieParam; @@ -46,32 +45,32 @@ */ public class FirebaseAuthSnippets { - public static void getUserById(String uid) throws InterruptedException, ExecutionException { + public static void getUserById(String uid) throws FirebaseAuthException { // [START get_user_by_id] - UserRecord userRecord = FirebaseAuth.getInstance().getUserAsync(uid).get(); + UserRecord userRecord = FirebaseAuth.getInstance().getUser(uid); // See the UserRecord reference doc for the contents of userRecord. System.out.println("Successfully fetched user data: " + userRecord.getUid()); // [END get_user_by_id] } - public static void getUserByEmail(String email) throws InterruptedException, ExecutionException { + public static void getUserByEmail(String email) throws FirebaseAuthException { // [START get_user_by_email] - UserRecord userRecord = FirebaseAuth.getInstance().getUserByEmailAsync(email).get(); + UserRecord userRecord = FirebaseAuth.getInstance().getUserByEmail(email); // See the UserRecord reference doc for the contents of userRecord. System.out.println("Successfully fetched user data: " + userRecord.getEmail()); // [END get_user_by_email] } public static void getUserByPhoneNumber( - String phoneNumber) throws InterruptedException, ExecutionException { + String phoneNumber) throws FirebaseAuthException { // [START get_user_by_phone] - UserRecord userRecord = FirebaseAuth.getInstance().getUserByPhoneNumberAsync(phoneNumber).get(); + UserRecord userRecord = FirebaseAuth.getInstance().getUserByPhoneNumber(phoneNumber); // See the UserRecord reference doc for the contents of userRecord. System.out.println("Successfully fetched user data: " + userRecord.getPhoneNumber()); // [END get_user_by_phone] } - public static void createUser() throws InterruptedException, ExecutionException { + public static void createUser() throws FirebaseAuthException { // [START create_user] CreateRequest request = new CreateRequest() .setEmail("user@example.com") @@ -82,24 +81,24 @@ public static void createUser() throws InterruptedException, ExecutionException .setPhotoUrl("http://www.example.com/12345678/photo.png") .setDisabled(false); - UserRecord userRecord = FirebaseAuth.getInstance().createUserAsync(request).get(); + UserRecord userRecord = FirebaseAuth.getInstance().createUser(request); System.out.println("Successfully created new user: " + userRecord.getUid()); // [END create_user] } - public static void createUserWithUid() throws InterruptedException, ExecutionException { + public static void createUserWithUid() throws FirebaseAuthException { // [START create_user_with_uid] CreateRequest request = new CreateRequest() .setUid("some-uid") .setEmail("user@example.com") .setPhoneNumber("+11234567890"); - UserRecord userRecord = FirebaseAuth.getInstance().createUserAsync(request).get(); + UserRecord userRecord = FirebaseAuth.getInstance().createUser(request); System.out.println("Successfully created new user: " + userRecord.getUid()); // [END create_user_with_uid] } - public static void updateUser(String uid) throws InterruptedException, ExecutionException { + public static void updateUser(String uid) throws FirebaseAuthException { // [START update_user] UpdateRequest request = new UpdateRequest(uid) .setEmail("user@example.com") @@ -110,18 +109,18 @@ public static void updateUser(String uid) throws InterruptedException, Execution .setPhotoUrl("http://www.example.com/12345678/photo.png") .setDisabled(true); - UserRecord userRecord = FirebaseAuth.getInstance().updateUserAsync(request).get(); + UserRecord userRecord = FirebaseAuth.getInstance().updateUser(request); System.out.println("Successfully updated user: " + userRecord.getUid()); // [END update_user] } public static void setCustomUserClaims( - String uid) throws InterruptedException, ExecutionException { + String uid) throws FirebaseAuthException { // [START set_custom_user_claims] // Set admin privilege on the user corresponding to uid. Map claims = new HashMap<>(); claims.put("admin", true); - FirebaseAuth.getInstance().setCustomUserClaimsAsync(uid, claims).get(); + FirebaseAuth.getInstance().setCustomUserClaims(uid, claims); // The new custom claims will propagate to the user's ID token the // next time a new one is issued. // [END set_custom_user_claims] @@ -129,7 +128,7 @@ public static void setCustomUserClaims( String idToken = "id_token"; // [START verify_custom_claims] // Verify the ID token first. - FirebaseToken decoded = FirebaseAuth.getInstance().verifyIdTokenAsync(idToken).get(); + FirebaseToken decoded = FirebaseAuth.getInstance().verifyIdToken(idToken); if (Boolean.TRUE.equals(decoded.getClaims().get("admin"))) { // Allow access to requested admin resource. } @@ -137,43 +136,43 @@ public static void setCustomUserClaims( // [START read_custom_user_claims] // Lookup the user associated with the specified uid. - UserRecord user = FirebaseAuth.getInstance().getUserAsync(uid).get(); + UserRecord user = FirebaseAuth.getInstance().getUser(uid); System.out.println(user.getCustomClaims().get("admin")); // [END read_custom_user_claims] } - public static void setCustomUserClaimsScript() throws InterruptedException, ExecutionException { + public static void setCustomUserClaimsScript() throws FirebaseAuthException { // [START set_custom_user_claims_script] UserRecord user = FirebaseAuth.getInstance() - .getUserByEmailAsync("user@admin.example.com").get(); + .getUserByEmail("user@admin.example.com"); // Confirm user is verified. if (user.isEmailVerified()) { Map claims = new HashMap<>(); claims.put("admin", true); - FirebaseAuth.getInstance().setCustomUserClaimsAsync(user.getUid(), claims).get(); + FirebaseAuth.getInstance().setCustomUserClaims(user.getUid(), claims); } // [END set_custom_user_claims_script] } - public static void setCustomUserClaimsInc() throws InterruptedException, ExecutionException { + public static void setCustomUserClaimsInc() throws FirebaseAuthException { // [START set_custom_user_claims_incremental] UserRecord user = FirebaseAuth.getInstance() - .getUserByEmailAsync("user@admin.example.com").get(); + .getUserByEmail("user@admin.example.com"); // Add incremental custom claim without overwriting the existing claims. Map currentClaims = user.getCustomClaims(); if (Boolean.TRUE.equals(currentClaims.get("admin"))) { // Add level. currentClaims.put("level", 10); // Add custom claims for additional privileges. - FirebaseAuth.getInstance().setCustomUserClaimsAsync(user.getUid(), currentClaims).get(); + FirebaseAuth.getInstance().setCustomUserClaims(user.getUid(), currentClaims); } // [END set_custom_user_claims_incremental] } - public static void listAllUsers() throws InterruptedException, ExecutionException { + public static void listAllUsers() throws FirebaseAuthException { // [START list_all_users] // Start listing users from the beginning, 1000 at a time. - ListUsersPage page = FirebaseAuth.getInstance().listUsersAsync(null).get(); + ListUsersPage page = FirebaseAuth.getInstance().listUsers(null); while (page != null) { for (ExportedUserRecord user : page.getValues()) { System.out.println("User: " + user.getUid()); @@ -183,82 +182,78 @@ public static void listAllUsers() throws InterruptedException, ExecutionExceptio // Iterate through all users. This will still retrieve users in batches, // buffering no more than 1000 users in memory at a time. - page = FirebaseAuth.getInstance().listUsersAsync(null).get(); + page = FirebaseAuth.getInstance().listUsers(null); for (ExportedUserRecord user : page.iterateAll()) { System.out.println("User: " + user.getUid()); } // [END list_all_users] } - public static void deleteUser(String uid) throws InterruptedException, ExecutionException { + public static void deleteUser(String uid) throws FirebaseAuthException { // [START delete_user] - FirebaseAuth.getInstance().deleteUserAsync(uid).get(); + FirebaseAuth.getInstance().deleteUser(uid); System.out.println("Successfully deleted user."); // [END delete_user] } - public static void createCustomToken() throws InterruptedException, ExecutionException { + public static void createCustomToken() throws FirebaseAuthException { // [START custom_token] String uid = "some-uid"; - String customToken = FirebaseAuth.getInstance().createCustomTokenAsync(uid).get(); + String customToken = FirebaseAuth.getInstance().createCustomToken(uid); // Send token back to client // [END custom_token] System.out.println("Created custom token: " + customToken); } - public static void createCustomTokenWithClaims() throws InterruptedException, ExecutionException { + public static void createCustomTokenWithClaims() throws FirebaseAuthException { // [START custom_token_with_claims] String uid = "some-uid"; Map additionalClaims = new HashMap(); additionalClaims.put("premiumAccount", true); String customToken = FirebaseAuth.getInstance() - .createCustomTokenAsync(uid, additionalClaims).get(); + .createCustomToken(uid, additionalClaims); // Send token back to client // [END custom_token_with_claims] System.out.println("Created custom token: " + customToken); } - public static void verifyIdToken( - String idToken) throws InterruptedException, ExecutionException { + public static void verifyIdToken(String idToken) throws FirebaseAuthException { // [START verify_id_token] // idToken comes from the client app (shown above) - FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdTokenAsync(idToken).get(); + FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdToken(idToken); String uid = decodedToken.getUid(); // [END verify_id_token] System.out.println("Decoded ID token from user: " + uid); } - public static void verifyIdTokenCheckRevoked(String idToken) throws InterruptedException { + public static void verifyIdTokenCheckRevoked(String idToken) { // [START verify_id_token_check_revoked] try { // Verify the ID token while checking if the token is revoked by passing checkRevoked // as true. boolean checkRevoked = true; FirebaseToken decodedToken = FirebaseAuth.getInstance() - .verifyIdTokenAsync(idToken, checkRevoked).get(); + .verifyIdToken(idToken, checkRevoked); // Token is valid and not revoked. String uid = decodedToken.getUid(); - } catch (ExecutionException e) { - if (e.getCause() instanceof FirebaseAuthException) { - FirebaseAuthException authError = (FirebaseAuthException) e.getCause(); - if (authError.getErrorCode().equals("id-token-revoked")) { - // Token has been revoked. Inform the user to reauthenticate or signOut() the user. - } else { - // Token is invalid. - } + } catch (FirebaseAuthException e) { + if (e.getErrorCode().equals("id-token-revoked")) { + // Token has been revoked. Inform the user to re-authenticate or signOut() the user. + } else { + // Token is invalid. } } // [END verify_id_token_check_revoked] } public static void revokeIdTokens( - String idToken) throws InterruptedException, ExecutionException { + String idToken) throws FirebaseAuthException { String uid = "someUid"; // [START revoke_tokens] - FirebaseAuth.getInstance().revokeRefreshTokensAsync(uid).get(); - UserRecord user = FirebaseAuth.getInstance().getUserAsync(uid).get(); + FirebaseAuth.getInstance().revokeRefreshTokens(uid); + UserRecord user = FirebaseAuth.getInstance().getUser(uid); // Convert to seconds as the auth_time in the token claims is in seconds too. long revocationSecond = user.getTokensValidAfterTimestamp() / 1000; System.out.println("Tokens revoked at: " + revocationSecond); @@ -268,7 +263,7 @@ public static void revokeIdTokens( DatabaseReference ref = FirebaseDatabase.getInstance().getReference("metadata/" + uid); Map userData = new HashMap<>(); userData.put("revokeTime", revocationSecond); - ref.setValueAsync(userData).get(); + ref.setValueAsync(userData); // [END save_revocation_in_db] } @@ -295,23 +290,22 @@ public Response createSessionCookie(LoginRequest request) { try { // Create the session cookie. This will also verify the ID token in the process. // The session cookie will have the same claims as the ID token. - String sessionCookie = FirebaseAuth.getInstance().createSessionCookieAsync( - idToken, options).get(); + String sessionCookie = FirebaseAuth.getInstance().createSessionCookie(idToken, options); // Set cookie policy parameters as required. NewCookie cookie = new NewCookie("session", sessionCookie /* ... other parameters */); return Response.ok().cookie(cookie).build(); - } catch (Exception e) { + } catch (FirebaseAuthException e) { return Response.status(Status.UNAUTHORIZED).entity("Failed to create a session cookie") .build(); } } // [END session_login] - public Response checkAuthTime(String idToken) throws Exception { + public Response checkAuthTime(String idToken) throws FirebaseAuthException { // [START check_auth_time] // To ensure that cookies are set only on recently signed in users, check auth_time in // ID token before creating a cookie. - FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdTokenAsync(idToken).get(); + FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdToken(idToken); long authTimeMillis = TimeUnit.SECONDS.toMillis( (long) decodedToken.getClaims().get("auth_time")); @@ -321,8 +315,7 @@ public Response checkAuthTime(String idToken) throws Exception { SessionCookieOptions options = SessionCookieOptions.builder() .setExpiresIn(expiresIn) .build(); - String sessionCookie = FirebaseAuth.getInstance().createSessionCookieAsync( - idToken, options).get(); + String sessionCookie = FirebaseAuth.getInstance().createSessionCookie(idToken, options); // Set cookie policy parameters as required. NewCookie cookie = new NewCookie("session", sessionCookie); return Response.ok().cookie(cookie).build(); @@ -350,10 +343,10 @@ public Response verifySessionCookie(@CookieParam("session") Cookie cookie) { // Verify the session cookie. In this case an additional check is added to detect // if the user's Firebase session was revoked, user deleted/disabled, etc. final boolean checkRevoked = true; - FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookieAsync( - sessionCookie, checkRevoked).get(); + FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookie( + sessionCookie, checkRevoked); return serveContentForUser(decodedToken); - } catch (Exception e) { + } catch (FirebaseAuthException e) { // Session cookie is unavailable, invalid or revoked. Force user to login. return Response.temporaryRedirect(URI.create("/login")).build(); } @@ -364,13 +357,13 @@ public Response checkPermissions(String sessionCookie) { // [START session_verify_with_permission_check] try { final boolean checkRevoked = true; - FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookieAsync( - sessionCookie, checkRevoked).get(); + FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookie( + sessionCookie, checkRevoked); if (Boolean.TRUE.equals(decodedToken.getClaims().get("admin"))) { return serveContentForAdmin(decodedToken); } return Response.status(Status.UNAUTHORIZED).entity("Insufficient permissions").build(); - } catch (Exception e) { + } catch (FirebaseAuthException e) { // Session cookie is unavailable, invalid or revoked. Force user to login. return Response.temporaryRedirect(URI.create("/login")).build(); } @@ -393,13 +386,12 @@ public Response clearSessionCookie(@CookieParam("session") Cookie cookie) { public Response clearSessionCookieAndRevoke(@CookieParam("session") Cookie cookie) { String sessionCookie = cookie.getValue(); try { - FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookieAsync( - sessionCookie).get(); - FirebaseAuth.getInstance().revokeRefreshTokensAsync(decodedToken.getUid()).get(); + FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookie(sessionCookie); + FirebaseAuth.getInstance().revokeRefreshTokens(decodedToken.getUid()); final int maxAge = 0; NewCookie newCookie = new NewCookie(cookie, null, maxAge, true); return Response.temporaryRedirect(URI.create("/login")).cookie(newCookie).build(); - } catch (Exception e) { + } catch (FirebaseAuthException e) { return Response.temporaryRedirect(URI.create("/login")).build(); } } diff --git a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java index 67bb62f2e..316e2062f 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java @@ -22,6 +22,7 @@ import com.google.firebase.messaging.Aps; import com.google.firebase.messaging.ApsAlert; import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; import com.google.firebase.messaging.TopicManagementResponse; @@ -35,7 +36,7 @@ */ public class FirebaseMessagingSnippets { - public void sendToToken() throws Exception { + public void sendToToken() throws FirebaseMessagingException { // [START send_to_token] // This registration token comes from the client FCM SDKs. String registrationToken = "YOUR_REGISTRATION_TOKEN"; @@ -49,13 +50,13 @@ public void sendToToken() throws Exception { // Send a message to the device corresponding to the provided // registration token. - String response = FirebaseMessaging.getInstance().sendAsync(message).get(); + String response = FirebaseMessaging.getInstance().send(message); // Response is a message ID string. System.out.println("Successfully sent message: " + response); // [END send_to_token] } - public void sendToTopic() throws Exception { + public void sendToTopic() throws FirebaseMessagingException { // [START send_to_topic] // The topic name can be optionally prefixed with "/topics/". String topic = "highScores"; @@ -68,13 +69,13 @@ public void sendToTopic() throws Exception { .build(); // Send a message to the devices subscribed to the provided topic. - String response = FirebaseMessaging.getInstance().sendAsync(message).get(); + String response = FirebaseMessaging.getInstance().send(message); // Response is a message ID string. System.out.println("Successfully sent message: " + response); // [END send_to_topic] } - public void sendToCondition() throws Exception { + public void sendToCondition() throws FirebaseMessagingException { // [START send_to_condition] // Define a condition which will send to devices which are subscribed // to either the Google stock or the tech industry topics. @@ -90,13 +91,13 @@ public void sendToCondition() throws Exception { // Send a message to devices subscribed to the combination of topics // specified by the provided condition. - String response = FirebaseMessaging.getInstance().sendAsync(message).get(); + String response = FirebaseMessaging.getInstance().send(message); // Response is a message ID string. System.out.println("Successfully sent message: " + response); // [END send_to_condition] } - public void sendDryRun() throws Exception { + public void sendDryRun() throws FirebaseMessagingException { Message message = Message.builder() .putData("score", "850") .putData("time", "2:45") @@ -106,7 +107,7 @@ public void sendDryRun() throws Exception { // [START send_dry_run] // Send a message in the dry run mode. boolean dryRun = true; - String response = FirebaseMessaging.getInstance().sendAsync(message, dryRun).get(); + String response = FirebaseMessaging.getInstance().send(message, dryRun); // Response is a message ID string. System.out.println("Dry run successful: " + response); // [END send_dry_run] @@ -189,7 +190,7 @@ public Message allPlatformsMessage() { return message; } - public void subscribeToTopic() throws Exception { + public void subscribeToTopic() throws FirebaseMessagingException { String topic = "highScores"; // [START subscribe] // These registration tokens come from the client FCM SDKs. @@ -201,15 +202,15 @@ public void subscribeToTopic() throws Exception { // Subscribe the devices corresponding to the registration tokens to the // topic. - TopicManagementResponse response = FirebaseMessaging.getInstance().subscribeToTopicAsync( - registrationTokens, topic).get(); + TopicManagementResponse response = FirebaseMessaging.getInstance().subscribeToTopic( + registrationTokens, topic); // See the TopicManagementResponse reference documentation // for the contents of response. System.out.println(response.getSuccessCount() + " tokens were subscribed successfully"); // [END subscribe] } - public void unsubscribeFromTopic() throws Exception { + public void unsubscribeFromTopic() throws FirebaseMessagingException { String topic = "highScores"; // [START unsubscribe] // These registration tokens come from the client FCM SDKs. @@ -221,8 +222,8 @@ public void unsubscribeFromTopic() throws Exception { // Unsubscribe the devices corresponding to the registration tokens from // the topic. - TopicManagementResponse response = FirebaseMessaging.getInstance().unsubscribeFromTopicAsync( - registrationTokens, topic).get(); + TopicManagementResponse response = FirebaseMessaging.getInstance().unsubscribeFromTopic( + registrationTokens, topic); // See the TopicManagementResponse reference documentation // for the contents of response. System.out.println(response.getSuccessCount() + " tokens were unsubscribed successfully"); From 75d6bb77d6bc9bd72a5bf443caf9708e8a9c0e27 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 15 May 2018 09:56:50 -0700 Subject: [PATCH 013/456] Staged release 6.1.0 (#176) * Updating CHANGELOG for 6.1.0 release. * [maven-release-plugin] prepare release v6.1.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 14f28a804..157d08ca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ - +# v6.1.0 + +- [changed] Deprecated the `FirebaseAuth.setCustomClaims()` method. + Developers should use the `FirebaseAuth.setCustomUserClaims()` method + instead. + # v6.0.0 - [added] `FirebaseAuth`, `FirebaseMessaging` and `FirebaseInstanceId` diff --git a/pom.xml b/pom.xml index 8e2190796..168bc0df5 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.0.1-SNAPSHOT + 6.1.1-SNAPSHOT jar firebase-admin From c5f7f2a9011eada07febf96f95be004f00fdaa1d Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 22 May 2018 10:03:14 -0700 Subject: [PATCH 014/456] Cleaning up the release tools (#173) --- pom.xml | 4 ++-- prepare_release.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 168bc0df5..32b90b01f 100644 --- a/pom.xml +++ b/pom.xml @@ -36,8 +36,8 @@ - Commercial - https://firebase.google.com/terms/ + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt diff --git a/prepare_release.sh b/prepare_release.sh index 47a7f4ed3..cc5969e0c 100755 --- a/prepare_release.sh +++ b/prepare_release.sh @@ -80,7 +80,7 @@ echo "[INFO] Updating CHANGELOG.md" if [ $HOST == "Darwin" ]; then sed -i "" -e "1 s/# Unreleased//" "CHANGELOG.md" else - sed -i -e "1 s/# Unreleased//" "CHANGELOG.md" + sed -i -e "/# Unreleased/d" "CHANGELOG.md" fi echo -e "# Unreleased\n\n-\n\n# v${VERSION}" | cat - CHANGELOG.md > TEMP_CHANGELOG.md From d6b3d4934af60b4ad3ea71db33876e38cc1d80df Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 22 May 2018 11:44:04 -0700 Subject: [PATCH 015/456] Bulk User Import API (#174) * Initial code for the importUsers() API * Added documentation and more tests * Added integration tests; Scrypt hash; more documentation * Added more tests * updated test * Adding other hash types * Added the remaining hash algorithms * Renamed to ImportUserRecord * Updated documentation * Some minor cleanup * Updated documentation * Updated documentation; Made hash required in UserImportOptions; Other minor nits * Renamed BasicHash to RepeatableHash; Made rounds range test configurable * Added javadoc comment --- CHANGELOG.md | 3 +- .../com/google/firebase/auth/ErrorInfo.java | 52 +++ .../google/firebase/auth/FirebaseAuth.java | 82 +++++ .../firebase/auth/FirebaseUserManager.java | 46 +++ .../firebase/auth/ImportUserRecord.java | 303 ++++++++++++++++++ .../google/firebase/auth/UserImportHash.java | 52 +++ .../firebase/auth/UserImportOptions.java | 95 ++++++ .../firebase/auth/UserImportResult.java | 74 +++++ .../google/firebase/auth/UserMetadata.java | 6 +- .../google/firebase/auth/UserProvider.java | 139 ++++++++ .../com/google/firebase/auth/UserRecord.java | 47 +-- .../com/google/firebase/auth/hash/Bcrypt.java | 41 +++ .../com/google/firebase/auth/hash/Hmac.java | 60 ++++ .../google/firebase/auth/hash/HmacMd5.java | 46 +++ .../google/firebase/auth/hash/HmacSha1.java | 46 +++ .../google/firebase/auth/hash/HmacSha256.java | 46 +++ .../google/firebase/auth/hash/HmacSha512.java | 46 +++ .../com/google/firebase/auth/hash/Md5.java | 46 +++ .../firebase/auth/hash/Pbkdf2Sha256.java | 46 +++ .../google/firebase/auth/hash/PbkdfSha1.java | 46 +++ .../firebase/auth/hash/RepeatableHash.java | 63 ++++ .../com/google/firebase/auth/hash/Scrypt.java | 116 +++++++ .../com/google/firebase/auth/hash/Sha1.java | 46 +++ .../com/google/firebase/auth/hash/Sha256.java | 46 +++ .../com/google/firebase/auth/hash/Sha512.java | 46 +++ .../firebase/auth/hash/StandardScrypt.java | 89 +++++ .../auth/internal/UploadAccountResponse.java | 49 +++ .../google/firebase/auth/FirebaseAuthIT.java | 82 +++++ .../auth/FirebaseUserManagerTest.java | 156 ++++++++- .../firebase/auth/ImportUserRecordTest.java | 212 ++++++++++++ .../firebase/auth/UserImportHashTest.java | 154 +++++++++ .../firebase/auth/UserImportOptionsTest.java | 50 +++ .../firebase/auth/UserProviderTest.java | 84 +++++ .../firebase/auth/hash/InvalidHashTest.java | 107 +++++++ src/test/resources/importUsersError.json | 6 + 35 files changed, 2603 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/google/firebase/auth/ErrorInfo.java create mode 100644 src/main/java/com/google/firebase/auth/ImportUserRecord.java create mode 100644 src/main/java/com/google/firebase/auth/UserImportHash.java create mode 100644 src/main/java/com/google/firebase/auth/UserImportOptions.java create mode 100644 src/main/java/com/google/firebase/auth/UserImportResult.java create mode 100644 src/main/java/com/google/firebase/auth/UserProvider.java create mode 100644 src/main/java/com/google/firebase/auth/hash/Bcrypt.java create mode 100644 src/main/java/com/google/firebase/auth/hash/Hmac.java create mode 100644 src/main/java/com/google/firebase/auth/hash/HmacMd5.java create mode 100644 src/main/java/com/google/firebase/auth/hash/HmacSha1.java create mode 100644 src/main/java/com/google/firebase/auth/hash/HmacSha256.java create mode 100644 src/main/java/com/google/firebase/auth/hash/HmacSha512.java create mode 100644 src/main/java/com/google/firebase/auth/hash/Md5.java create mode 100644 src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java create mode 100644 src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java create mode 100644 src/main/java/com/google/firebase/auth/hash/RepeatableHash.java create mode 100644 src/main/java/com/google/firebase/auth/hash/Scrypt.java create mode 100644 src/main/java/com/google/firebase/auth/hash/Sha1.java create mode 100644 src/main/java/com/google/firebase/auth/hash/Sha256.java create mode 100644 src/main/java/com/google/firebase/auth/hash/Sha512.java create mode 100644 src/main/java/com/google/firebase/auth/hash/StandardScrypt.java create mode 100644 src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java create mode 100644 src/test/java/com/google/firebase/auth/ImportUserRecordTest.java create mode 100644 src/test/java/com/google/firebase/auth/UserImportHashTest.java create mode 100644 src/test/java/com/google/firebase/auth/UserImportOptionsTest.java create mode 100644 src/test/java/com/google/firebase/auth/UserProviderTest.java create mode 100644 src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java create mode 100644 src/test/resources/importUsersError.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 157d08ca0..26bb3ef41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased -- +- [added] Added new `importUsersAsync()` API for bulk importing users + into Firebase Auth. # v6.1.0 diff --git a/src/main/java/com/google/firebase/auth/ErrorInfo.java b/src/main/java/com/google/firebase/auth/ErrorInfo.java new file mode 100644 index 000000000..7f7a5e346 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/ErrorInfo.java @@ -0,0 +1,52 @@ +/* + * 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 java.util.List; + +/** + * Represents an error encountered while importing an {@link ImportUserRecord}. + */ +public final class ErrorInfo { + + private final int index; + private final String reason; + + ErrorInfo(int index, String reason) { + this.index = index; + this.reason = reason; + } + + /** + * The index of the failed user in the list passed to the + * {@link FirebaseAuth#importUsersAsync(List, UserImportOptions)} method. + * + * @return an integer index. + */ + public int getIndex() { + return index; + } + + /** + * A string describing the error. + * + * @return A string error message. + */ + public String getReason() { + return reason; + } +} diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index ceaf33485..47dd3ae6a 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -29,6 +29,7 @@ import com.google.common.base.Strings; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseUserManager.UserImportRequest; import com.google.firebase.auth.ListUsersPage.DefaultUserSource; import com.google.firebase.auth.ListUsersPage.PageFactory; import com.google.firebase.auth.UserRecord.CreateRequest; @@ -42,6 +43,7 @@ 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; @@ -860,6 +862,86 @@ protected Void execute() throws FirebaseAuthException { }; } + /** + * Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a + * time. This operation is optimized for bulk imports and will ignore checks on identifier + * uniqueness which could result in duplications. + * + *

    {@link UserImportOptions} is required to import users with passwords. See + * {@link #importUsers(List, UserImportOptions)}. + * + * @param users A non-empty list of users to be imported. Length must not exceed 1000. + * @return A {@link UserImportResult} instance. + * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 + * elements. Or if at least one user specifies a password. + * @throws FirebaseAuthException If an error occurs while importing users. + */ + public UserImportResult importUsers(List users) throws FirebaseAuthException { + return importUsers(users, null); + } + + /** + * Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a + * time. This operation is optimized for bulk imports and will ignore checks on identifier + * uniqueness which could result in duplications. + * + * @param users A non-empty list of users to be imported. Length must not exceed 1000. + * @param options a {@link UserImportOptions} instance or null. Required when importing users + * with passwords. + * @return A {@link UserImportResult} instance. + * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 + * elements. Or if at least one user specifies a password, and options is null. + * @throws FirebaseAuthException If an error occurs while importing users. + */ + public UserImportResult importUsers(List users, + @Nullable UserImportOptions options) throws FirebaseAuthException { + return importUsersOp(users, options).call(); + } + + /** + * Similar to {@link #importUsers(List)} but performs the operation asynchronously. + * + * @param users A non-empty list of users to be imported. Length must not exceed 1000. + * @return An {@code ApiFuture} which will complete successfully when the user accounts are + * imported. If an error occurs while importing the users, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 + * elements. Or if at least one user specifies a password. + */ + public ApiFuture importUsersAsync(List users) { + return importUsersAsync(users, null); + } + + /** + * Similar to {@link #importUsers(List, UserImportOptions)} but performs the operation + * asynchronously. + * + * @param users A non-empty list of users to be imported. Length must not exceed 1000. + * @param options a {@link UserImportOptions} instance or null. Required when importing users + * with passwords. + * @return An {@code ApiFuture} which will complete successfully when the user accounts are + * imported. If an error occurs while importing the users, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 + * elements. Or if at least one user specifies a password, and options is null. + */ + public ApiFuture importUsersAsync(List users, + @Nullable UserImportOptions options) { + return importUsersOp(users, options).callAsync(firebaseApp); + } + + private CallableOperation importUsersOp( + List users, UserImportOptions options) { + checkNotDestroyed(); + final UserImportRequest request = new UserImportRequest(users, options, jsonFactory); + return new CallableOperation() { + @Override + protected UserImportResult execute() throws FirebaseAuthException { + return userManager.importUsers(request); + } + }; + } + @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 67720b6d2..39b3e3780 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -30,6 +30,7 @@ import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.util.Key; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; @@ -41,6 +42,7 @@ import com.google.firebase.auth.internal.GetAccountInfoResponse; import com.google.firebase.auth.internal.HttpErrorResponse; +import com.google.firebase.auth.internal.UploadAccountResponse; import com.google.firebase.internal.FirebaseRequestInitializer; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.SdkUtils; @@ -82,6 +84,7 @@ class FirebaseUserManager { .build(); static final int MAX_LIST_USERS_RESULTS = 1000; + static final int MAX_IMPORT_USERS = 1000; static final List RESERVED_CLAIMS = ImmutableList.of( "amr", "at_hash", "aud", "auth_time", "azp", "cnf", "c_hash", "exp", "iat", @@ -195,6 +198,15 @@ DownloadAccountResponse listUsers(int maxResults, String pageToken) throws Fireb return response; } + UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthException { + checkNotNull(request); + UploadAccountResponse response = post("uploadAccount", request, UploadAccountResponse.class); + if (response == null) { + throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to import users."); + } + return new UserImportResult(request.getUsersCount(), response); + } + String createSessionCookie(String idToken, SessionCookieOptions options) throws FirebaseAuthException { final Map payload = ImmutableMap.of( @@ -257,4 +269,38 @@ private void handleHttpError(HttpResponseException e) throws FirebaseAuthExcepti "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); throw new FirebaseAuthException(INTERNAL_ERROR, msg, e); } + + static class UserImportRequest extends GenericJson { + + @Key("users") + private final List> users; + + UserImportRequest(List users, UserImportOptions options, + JsonFactory jsonFactory) { + checkArgument(users != null && !users.isEmpty(), "users must not be null or empty"); + checkArgument(users.size() <= FirebaseUserManager.MAX_IMPORT_USERS, + "users list must not contain more than %s items", FirebaseUserManager.MAX_IMPORT_USERS); + + boolean hasPassword = false; + ImmutableList.Builder> usersBuilder = ImmutableList.builder(); + for (ImportUserRecord user : users) { + if (user.hasPassword()) { + hasPassword = true; + } + usersBuilder.add(user.getProperties(jsonFactory)); + } + this.users = usersBuilder.build(); + + if (hasPassword) { + checkArgument(options != null && options.getHash() != null, + "UserImportHash option is required when at least one user has a password. Provide " + + "a UserImportHash via UserImportOptions.withHash()."); + this.putAll(options.getProperties()); + } + } + + int getUsersCount() { + return users.size(); + } + } } diff --git a/src/main/java/com/google/firebase/auth/ImportUserRecord.java b/src/main/java/com/google/firebase/auth/ImportUserRecord.java new file mode 100644 index 000000000..5ce57fcc9 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/ImportUserRecord.java @@ -0,0 +1,303 @@ +/* + * 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.auth; + +import com.google.api.client.json.JsonFactory; +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.internal.NonNull; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a user account to be imported to Firebase Auth via the + * {@link FirebaseAuth#importUsers(List, UserImportOptions)} API. Must contain at least a + * uid string. + */ +public final class ImportUserRecord { + + private final Map properties; + + private ImportUserRecord(Map properties) { + this.properties = ImmutableMap.copyOf(properties); + } + + Map getProperties(JsonFactory jsonFactory) { + Map copy = new HashMap<>(properties); + // serialize custom claims + if (copy.containsKey(UserRecord.CUSTOM_ATTRIBUTES)) { + Map customClaims = (Map) copy.remove(UserRecord.CUSTOM_ATTRIBUTES); + copy.put(UserRecord.CUSTOM_ATTRIBUTES, UserRecord.serializeCustomClaims( + customClaims, jsonFactory)); + } + return ImmutableMap.copyOf(copy); + } + + boolean hasPassword() { + return this.properties.containsKey("passwordHash"); + } + + /** + * Creates a new {@link ImportUserRecord.Builder}. + * + * @return A {@link ImportUserRecord.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String uid; + private String email; + private Boolean emailVerified; + private String displayName; + private String phoneNumber; + private String photoUrl; + private Boolean disabled; + private UserMetadata userMetadata; + private byte[] passwordHash; + private byte[] passwordSalt; + + private final List userProviders = new ArrayList<>(); + private final Map customClaims = new HashMap<>(); + + private Builder() {} + + /** + * Sets a user ID for the user. + * + * @param uid a non-null, non-empty user ID that uniquely identifies the user. The user ID + * must not be longer than 128 characters. + * @return This builder. + */ + public Builder setUid(String uid) { + this.uid = uid; + return this; + } + + /** + * Sets an email address for the user. + * + * @param email a non-null, non-empty email address string. + * @return This builder. + */ + public Builder setEmail(String email) { + this.email = email; + return this; + } + + /** + * Sets whether the user email address has been verified or not. + * + * @param emailVerified a boolean indicating the email verification status. + * @return This builder. + */ + public Builder setEmailVerified(boolean emailVerified) { + this.emailVerified = emailVerified; + return this; + } + + /** + * Sets the display name for the user. + * + * @param displayName a non-null, non-empty display name string. + * @return This builder. + */ + public Builder setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + + /** + * Sets the phone number associated with this user. + * + * @param phoneNumber a valid phone number string. + * @return This builder. + */ + public Builder setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + return this; + } + + /** + * Sets the photo URL for the user. + * + * @param photoUrl a non-null, non-empty URL string. + * @return This builder. + */ + public Builder setPhotoUrl(String photoUrl) { + this.photoUrl = photoUrl; + return this; + } + + /** + * Sets whether the user account should be disabled by default or not. + * + * @param disabled a boolean indicating whether the account should be disabled. + * @return This builder. + */ + public Builder setDisabled(boolean disabled) { + this.disabled = disabled; + return this; + } + + /** + * Sets additional metadata about the user. + * + * @param userMetadata A {@link UserMetadata} instance. + * @return This builder. + */ + public Builder setUserMetadata(UserMetadata userMetadata) { + this.userMetadata = userMetadata; + return this; + } + + /** + * Sets a byte array representing the user's hashed password. If at least one user account + * carries a password hash, a {@link UserImportHash} must be specified when calling the + * {@link FirebaseAuth#importUsersAsync(List, UserImportOptions)} method. See + * {@link UserImportOptions.Builder#setHash(UserImportHash)}. + * + * @param passwordHash A byte array. + * @return This builder. + */ + public Builder setPasswordHash(byte[] passwordHash) { + this.passwordHash = passwordHash; + return this; + } + + /** + * Sets a byte array representing the user's password salt. + * + * @param passwordSalt A byte array. + * @return This builder. + */ + public Builder setPasswordSalt(byte[] passwordSalt) { + this.passwordSalt = passwordSalt; + return this; + } + + /** + * Adds a user provider to be associated with this user. + * + *

    A {@link UserProvider} represents the identity of the user as specified by an + * identity provider that is linked to this user account. The identity provider can specify + * its own values for common user attributes like email, display name and photo URL. + * + * @param provider A non-null {@link UserProvider}. + * @return This builder. + */ + public Builder addUserProvider(@NonNull UserProvider provider) { + this.userProviders.add(provider); + return this; + } + + /** + * Associates all user provider's in the given list with this user. + * + * @param providers A list of {@link UserProvider} instances. + * @return This builder. + */ + public Builder addAllUserProviders(List providers) { + this.userProviders.addAll(providers); + return this; + } + + /** + * Sets the specified custom claim on this user account. + * + * @param key Name of the claim. + * @param value Value of the claim. + * @return This builder. + */ + public Builder putCustomClaim(String key, Object value) { + this.customClaims.put(key, value); + return this; + } + + /** + * Sets the custom claims associated with this user. + * + * @param customClaims a Map of custom claims + */ + public Builder putAllCustomClaims(Map customClaims) { + this.customClaims.putAll(customClaims); + return this; + } + + /** + * Builds a new {@link ImportUserRecord}. + * + * @return A non-null {@link ImportUserRecord}. + */ + public ImportUserRecord build() { + Map properties = new HashMap<>(); + UserRecord.checkUid(uid); + properties.put("localId", uid); + + if (!Strings.isNullOrEmpty(email)) { + UserRecord.checkEmail(email); + properties.put("email", email); + } + if (!Strings.isNullOrEmpty(photoUrl)) { + UserRecord.checkUrl(photoUrl); + properties.put("photoUrl", photoUrl); + } + if (!Strings.isNullOrEmpty(phoneNumber)) { + UserRecord.checkPhoneNumber(phoneNumber); + properties.put("phoneNumber", phoneNumber); + } + if (!Strings.isNullOrEmpty(displayName)) { + properties.put("displayName", displayName); + } + if (userMetadata != null) { + if (userMetadata.getCreationTimestamp() > 0) { + properties.put("createdAt", userMetadata.getCreationTimestamp()); + } + if (userMetadata.getLastSignInTimestamp() > 0) { + properties.put("lastLoginAt", userMetadata.getLastSignInTimestamp()); + } + } + if (passwordHash != null) { + properties.put("passwordHash", BaseEncoding.base64Url().encode(passwordHash)); + } + if (passwordSalt != null) { + properties.put("salt", BaseEncoding.base64Url().encode(passwordSalt)); + } + if (userProviders.size() > 0) { + properties.put("providerUserInfo", ImmutableList.copyOf(userProviders)); + } + if (customClaims.size() > 0) { + ImmutableMap mergedClaims = ImmutableMap.copyOf(customClaims); + UserRecord.checkCustomClaims(mergedClaims); + properties.put(UserRecord.CUSTOM_ATTRIBUTES, mergedClaims); + } + if (emailVerified != null) { + properties.put("emailVerified", emailVerified); + } + if (disabled != null) { + properties.put("disabled", disabled); + } + return new ImportUserRecord(properties); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/UserImportHash.java b/src/main/java/com/google/firebase/auth/UserImportHash.java new file mode 100644 index 000000000..443c6f858 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/UserImportHash.java @@ -0,0 +1,52 @@ +/* + * 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.auth; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import java.util.Map; + +/** + * Represents a hash algorithm and the related configuration parameters used to hash user + * passwords. An instance of this class must be specified if importing any users with password + * hashes (see {@link UserImportOptions.Builder#setHash(UserImportHash)}. + * + *

    This is not expected to be extended in user code. Applications should use one of the provided + * concrete implementations in the {@link com.google.firebase.auth.hash} package. See + * documentation for more + * details on available options. + */ +public abstract class UserImportHash { + + private final String name; + + protected UserImportHash(String name) { + checkArgument(!Strings.isNullOrEmpty(name)); + this.name = name; + } + + final Map getProperties() { + return ImmutableMap.builder() + .put("hashAlgorithm", name) + .putAll(getOptions()) + .build(); + } + + protected abstract Map getOptions(); +} diff --git a/src/main/java/com/google/firebase/auth/UserImportOptions.java b/src/main/java/com/google/firebase/auth/UserImportOptions.java new file mode 100644 index 000000000..86accf91b --- /dev/null +++ b/src/main/java/com/google/firebase/auth/UserImportOptions.java @@ -0,0 +1,95 @@ +/* + * 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.checkNotNull; + +import com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.NonNull; +import java.util.List; +import java.util.Map; + +/** + * A collection of options that can be passed to the + * {@link FirebaseAuth#importUsersAsync(List, UserImportOptions)} API. + */ +public final class UserImportOptions { + + private final UserImportHash hash; + + private UserImportOptions(Builder builder) { + this.hash = checkNotNull(builder.hash); + } + + /** + * Creates a new {@link UserImportOptions} containing the provided hash algorithm. + * + * @param hash A non-null {@link UserImportHash}. + * @return A new {@link UserImportOptions}. + */ + public static UserImportOptions withHash(@NonNull UserImportHash hash) { + return builder().setHash(checkNotNull(hash)).build(); + } + + /** + * Creates a new {@link UserImportOptions.Builder}. + * + * @return A {@link UserImportOptions.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + Map getProperties() { + return ImmutableMap.builder() + .putAll(hash.getProperties()) + .build(); + } + + UserImportHash getHash() { + return hash; + } + + public static class Builder { + + private UserImportHash hash; + + private Builder() {} + + /** + * Sets the algorithm used to hash user passwords. This is required + * when at least one of the {@link ImportUserRecord} instances being imported has a password + * hash. See {@link ImportUserRecord.Builder#setPasswordHash(byte[])}. + * + * @param hash A {@link UserImportHash}. + * @return This builder. + */ + public Builder setHash(@NonNull UserImportHash hash) { + this.hash = hash; + return this; + } + + /** + * Builds a new {@link UserImportOptions}. + * + * @return A non-null {@link UserImportOptions}. + */ + public UserImportOptions build() { + return new UserImportOptions(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/UserImportResult.java b/src/main/java/com/google/firebase/auth/UserImportResult.java new file mode 100644 index 000000000..143b0618b --- /dev/null +++ b/src/main/java/com/google/firebase/auth/UserImportResult.java @@ -0,0 +1,74 @@ +/* + * 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.collect.ImmutableList; +import com.google.firebase.auth.internal.UploadAccountResponse; +import com.google.firebase.internal.NonNull; +import java.util.List; + +/** + * Represents the result of the {@link FirebaseAuth#importUsersAsync(List, UserImportOptions)} API. + */ +public final class UserImportResult { + + private final int users; + private final ImmutableList errors; + + UserImportResult(int users, UploadAccountResponse response) { + ImmutableList.Builder errorsBuilder = ImmutableList.builder(); + List errors = response.getErrors(); + if (errors != null) { + checkArgument(users >= errors.size()); + for (UploadAccountResponse.ErrorInfo error : errors) { + errorsBuilder.add(new ErrorInfo(error.getIndex(), error.getMessage())); + } + } + this.users = users; + this.errors = errorsBuilder.build(); + } + + /** + * Returns the number of users that were imported successfully. + * + * @return number of users successfully imported (possibly zero). + */ + public int getSuccessCount() { + return users - errors.size(); + } + + /** + * Returns the number of users that failed to be imported. + * + * @return number of users that resulted in import failures (possibly zero). + */ + public int getFailureCount() { + return errors.size(); + } + + /** + * A list of {@link ErrorInfo} instances describing the errors that were encountered during + * the import. Length of this list is equal to the return value of {@link #getFailureCount()}. + * + * @return A non-null list (possibly empty). + */ + @NonNull public List getErrors() { + return errors; + } +} diff --git a/src/main/java/com/google/firebase/auth/UserMetadata.java b/src/main/java/com/google/firebase/auth/UserMetadata.java index eb4da93c8..a2872371f 100644 --- a/src/main/java/com/google/firebase/auth/UserMetadata.java +++ b/src/main/java/com/google/firebase/auth/UserMetadata.java @@ -24,7 +24,11 @@ public class UserMetadata { private final long creationTimestamp; private final long lastSignInTimestamp; - UserMetadata(long creationTimestamp, long lastSignInTimestamp) { + public UserMetadata(long creationTimestamp) { + this(creationTimestamp, 0L); + } + + public UserMetadata(long creationTimestamp, long lastSignInTimestamp) { this.creationTimestamp = creationTimestamp; this.lastSignInTimestamp = lastSignInTimestamp; } diff --git a/src/main/java/com/google/firebase/auth/UserProvider.java b/src/main/java/com/google/firebase/auth/UserProvider.java new file mode 100644 index 000000000..a13e0ecc1 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/UserProvider.java @@ -0,0 +1,139 @@ +/* + * 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.auth; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.util.Key; +import com.google.common.base.Strings; + +/** + * Represents a user identity provider that can be associated with a Firebase user. + */ +public final class UserProvider { + + @Key("rawId") + private final String uid; + + @Key("displayName") + private final String displayName; + + @Key("email") + private final String email; + + @Key("photoUrl") + private final String photoUrl; + + @Key("providerId") + private final String providerId; + + private UserProvider(Builder builder) { + checkArgument(!Strings.isNullOrEmpty(builder.uid), "Uid must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(builder.providerId), + "ProviderId must not be null or empty"); + this.uid = builder.uid; + this.displayName = builder.displayName; + this.email = builder.email; + this.photoUrl = builder.photoUrl; + this.providerId = builder.providerId; + } + + /** + * Creates a new {@link UserProvider.Builder}. + * + * @return A {@link UserProvider.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String uid; + private String displayName; + private String email; + private String photoUrl; + private String providerId; + + private Builder() {} + + /** + * Sets the user's unique ID assigned by the identity provider. This field is required. + * + * @param uid a user ID string. + * @return This builder. + */ + public Builder setUid(String uid) { + this.uid = uid; + return this; + } + + /** + * Sets the user's display name. + * + * @param displayName display name of the user. + * @return This builder. + */ + public Builder setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + + /** + * Sets the user's email address. + * + * @param email an email address string. + * @return This builder. + */ + public Builder setEmail(String email) { + this.email = email; + return this; + } + + /** + * Sets the photo URl of the user. + * + * @param photoUrl a photo URL string. + * @return This builder. + */ + public Builder setPhotoUrl(String photoUrl) { + this.photoUrl = photoUrl; + return this; + } + + /** + * Sets the ID of the identity provider. This can be a short domain name (e.g. google.com) or + * the identifier of an OpenID identity provider. This field is required. + * + * @param providerId an ID string that uniquely identifies the identity provider. + * @return This builder. + */ + public Builder setProviderId(String providerId) { + this.providerId = providerId; + return this; + } + + /** + * Builds a new {@link UserProvider}. + * + * @return A non-null {@link UserProvider}. + */ + public UserProvider build() { + return new UserProvider(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index cc27bd197..e00450079 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -18,10 +18,8 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; import com.google.api.client.json.JsonFactory; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -33,7 +31,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -48,7 +45,7 @@ public class UserRecord implements UserInfo { private static final Map REMOVABLE_FIELDS = ImmutableMap.of( "displayName", "DISPLAY_NAME", "photoUrl", "PHOTO_URL"); - private static final String CUSTOM_ATTRIBUTES = "customAttributes"; + static final String CUSTOM_ATTRIBUTES = "customAttributes"; private static final int MAX_CLAIMS_PAYLOAD_SIZE = 1000; private final String uid; @@ -232,29 +229,44 @@ public UpdateRequest updateRequest() { return new UpdateRequest(uid); } - private static void checkEmail(String email) { + static void checkUid(String uid) { + checkArgument(!Strings.isNullOrEmpty(uid), "uid cannot be null or empty"); + checkArgument(uid.length() <= 128, "UID cannot be longer than 128 characters"); + } + + static void checkEmail(String email) { checkArgument(!Strings.isNullOrEmpty(email), "email cannot be null or empty"); checkArgument(email.matches("^[^@]+@[^@]+$")); } - private static void checkPhoneNumber(String phoneNumber) { + static void checkPhoneNumber(String phoneNumber) { // Phone number verification is very lax here. Backend will enforce E.164 spec compliance, and // normalize accordingly. checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number cannot be null or empty"); - checkState(phoneNumber.startsWith("+"), + checkArgument(phoneNumber.startsWith("+"), "phone number must be a valid, E.164 compliant identifier starting with a '+' sign"); } + static void checkUrl(String photoUrl) { + checkArgument(!Strings.isNullOrEmpty(photoUrl), "url cannot be null or empty"); + try { + new URL(photoUrl); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("malformed url string", e); + } + } + private static void checkPassword(String password) { checkArgument(!Strings.isNullOrEmpty(password), "password cannot be null or empty"); checkArgument(password.length() >= 6, "password must be at least 6 characters long"); } - private static void checkCustomClaims(Map customClaims) { + static void checkCustomClaims(Map customClaims) { if (customClaims == null) { return; } for (String key : customClaims.keySet()) { + checkArgument(!Strings.isNullOrEmpty(key), "Claim names must not be null or empty"); checkArgument(!FirebaseUserManager.RESERVED_CLAIMS.contains(key), "Claim '" + key + "' is reserved and cannot be set"); } @@ -265,7 +277,7 @@ private static void checkValidSince(long epochSeconds) { + Long.toString(epochSeconds)); } - private static String serializeCustomClaims(Map customClaims, JsonFactory jsonFactory) { + static String serializeCustomClaims(Map customClaims, JsonFactory jsonFactory) { checkNotNull(jsonFactory, "JsonFactory must not be null"); if (customClaims == null || customClaims.isEmpty()) { return "{}"; @@ -305,8 +317,7 @@ public CreateRequest() { * must not be longer than 128 characters. */ public CreateRequest setUid(String uid) { - checkArgument(!Strings.isNullOrEmpty(uid), "uid cannot be null or empty"); - checkArgument(uid.length() <= 128, "UID cannot be longer than 128 characters"); + checkUid(uid); properties.put("localId", uid); return this; } @@ -360,12 +371,7 @@ public CreateRequest setDisplayName(String displayName) { * @param photoUrl a non-null, non-empty URL string. */ public CreateRequest setPhotoUrl(String photoUrl) { - checkArgument(!Strings.isNullOrEmpty(photoUrl), "photoUrl cannot be null or empty"); - try { - new URL(photoUrl); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("malformed photoUrl string", e); - } + checkUrl(photoUrl); properties.put("photoUrl", photoUrl); return this; } @@ -475,12 +481,9 @@ public UpdateRequest setDisplayName(@Nullable String displayName) { * @param photoUrl a valid URL string or null */ public UpdateRequest setPhotoUrl(@Nullable String photoUrl) { + // This is allowed to be null if (photoUrl != null) { - try { - new URL(photoUrl); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("malformed photoUrl string", e); - } + checkUrl(photoUrl); } properties.put("photoUrl", photoUrl); return this; diff --git a/src/main/java/com/google/firebase/auth/hash/Bcrypt.java b/src/main/java/com/google/firebase/auth/hash/Bcrypt.java new file mode 100644 index 000000000..2b5f89029 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/Bcrypt.java @@ -0,0 +1,41 @@ +/* + * 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.hash; + +import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.UserImportHash; +import java.util.Map; + +/** + * Represents the Bcrypt password hashing algorithm. Can be used as an instance of + * {@link com.google.firebase.auth.UserImportHash} when importing users. + */ +public class Bcrypt extends UserImportHash { + + private Bcrypt() { + super("BCRYPT"); + } + + public static Bcrypt getInstance() { + return new Bcrypt(); + } + + @Override + protected Map getOptions() { + return ImmutableMap.of(); + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/Hmac.java b/src/main/java/com/google/firebase/auth/hash/Hmac.java new file mode 100644 index 000000000..d55879389 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/Hmac.java @@ -0,0 +1,60 @@ +/* + * 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.hash; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.ImmutableMap; +import com.google.common.io.BaseEncoding; +import com.google.firebase.auth.UserImportHash; +import java.util.Map; + +abstract class Hmac extends UserImportHash { + + private final String key; + + Hmac(String name, Builder builder) { + super(name); + checkArgument(builder.key != null && builder.key.length > 0, + "A non-empty key is required for HMAC algorithms"); + this.key = BaseEncoding.base64().encode(builder.key); + } + + @Override + protected final Map getOptions() { + return ImmutableMap.of("signerKey", key); + } + + abstract static class Builder { + private byte[] key; + + protected abstract T getInstance(); + + /** + * Sets the signer key for the HMAC hash algorithm. Required field. + * + * @param key Signer key as a byte array. + * @return This builder. + */ + public T setKey(byte[] key) { + this.key = key; + return getInstance(); + } + + public abstract U build(); + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/HmacMd5.java b/src/main/java/com/google/firebase/auth/hash/HmacMd5.java new file mode 100644 index 000000000..b67574358 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/HmacMd5.java @@ -0,0 +1,46 @@ +/* + * 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.hash; + +/** + * Represents the HMAC MD5 password hashing algorithm. Can be used as an instance of + * {@link com.google.firebase.auth.UserImportHash} when importing users. + */ +public class HmacMd5 extends Hmac { + + private HmacMd5(Builder builder) { + super("HMAC_MD5", builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends Hmac.Builder { + + private Builder() {} + + @Override + protected Builder getInstance() { + return this; + } + + public HmacMd5 build() { + return new HmacMd5(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/HmacSha1.java b/src/main/java/com/google/firebase/auth/hash/HmacSha1.java new file mode 100644 index 000000000..a9ecefd6f --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/HmacSha1.java @@ -0,0 +1,46 @@ +/* + * 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.hash; + +/** + * Represents the HMAC SHA1 password hashing algorithm. Can be used as an instance of + * {@link com.google.firebase.auth.UserImportHash} when importing users. + */ +public class HmacSha1 extends Hmac { + + private HmacSha1(Builder builder) { + super("HMAC_SHA1", builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends Hmac.Builder { + + private Builder() {} + + @Override + protected Builder getInstance() { + return this; + } + + public HmacSha1 build() { + return new HmacSha1(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/HmacSha256.java b/src/main/java/com/google/firebase/auth/hash/HmacSha256.java new file mode 100644 index 000000000..78f131cff --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/HmacSha256.java @@ -0,0 +1,46 @@ +/* + * 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.hash; + +/** + * Represents the HMAC SHA256 password hashing algorithm. Can be used as an instance of + * {@link com.google.firebase.auth.UserImportHash} when importing users. + */ +public class HmacSha256 extends Hmac { + + private HmacSha256(Builder builder) { + super("HMAC_SHA256", builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends Hmac.Builder { + + private Builder() {} + + @Override + protected Builder getInstance() { + return this; + } + + public HmacSha256 build() { + return new HmacSha256(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/HmacSha512.java b/src/main/java/com/google/firebase/auth/hash/HmacSha512.java new file mode 100644 index 000000000..21e6a2b25 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/HmacSha512.java @@ -0,0 +1,46 @@ +/* + * 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.auth.hash; + +/** + * Represents the HMAC SHA512 password hashing algorithm. Can be used as an instance of + * {@link com.google.firebase.auth.UserImportHash} when importing users. + */ +public class HmacSha512 extends Hmac { + + private HmacSha512(Builder builder) { + super("HMAC_SHA512", builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends Hmac.Builder { + + private Builder() {} + + @Override + protected Builder getInstance() { + return this; + } + + public HmacSha512 build() { + return new HmacSha512(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/Md5.java b/src/main/java/com/google/firebase/auth/hash/Md5.java new file mode 100644 index 000000000..f45362ae3 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/Md5.java @@ -0,0 +1,46 @@ +/* + * 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.hash; + +/** + * Represents the MD5 password hashing algorithm. Can be used as an instance of + * {@link com.google.firebase.auth.UserImportHash} when importing users. + */ +public class Md5 extends RepeatableHash { + + private Md5(Builder builder) { + super("MD5", 0, 120000, builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends RepeatableHash.Builder { + + private Builder() {} + + @Override + protected Builder getInstance() { + return this; + } + + public Md5 build() { + return new Md5(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java b/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java new file mode 100644 index 000000000..4c5108e35 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java @@ -0,0 +1,46 @@ +/* + * 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.hash; + +/** + * Represents the PBKDF2 SHA256 password hashing algorithm. Can be used as an instance of + * {@link com.google.firebase.auth.UserImportHash} when importing users. + */ +public class Pbkdf2Sha256 extends RepeatableHash { + + private Pbkdf2Sha256(Builder builder) { + super("PBKDF2_SHA256", 0, 120000, builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends RepeatableHash.Builder { + + private Builder() {} + + @Override + protected Builder getInstance() { + return this; + } + + public Pbkdf2Sha256 build() { + return new Pbkdf2Sha256(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java b/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java new file mode 100644 index 000000000..8afe3f4ab --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java @@ -0,0 +1,46 @@ +/* + * 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.hash; + +/** + * Represents the PBKDF SHA1 password hashing algorithm. Can be used as an instance of + * {@link com.google.firebase.auth.UserImportHash} when importing users. + */ +public class PbkdfSha1 extends RepeatableHash { + + private PbkdfSha1(Builder builder) { + super("PBKDF_SHA1", 0, 120000, builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends RepeatableHash.Builder { + + private Builder() {} + + @Override + protected Builder getInstance() { + return this; + } + + public PbkdfSha1 build() { + return new PbkdfSha1(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/RepeatableHash.java b/src/main/java/com/google/firebase/auth/hash/RepeatableHash.java new file mode 100644 index 000000000..bcf1a9524 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/RepeatableHash.java @@ -0,0 +1,63 @@ +/* + * 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.hash; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.UserImportHash; +import java.util.Map; + +/** + * An abstract {@link UserImportHash} implementation that accepts a {@code rounds} parameter in + * a given range. + */ +abstract class RepeatableHash extends UserImportHash { + + private final int rounds; + + RepeatableHash(String name, int min, int max, Builder builder) { + super(name); + checkArgument(builder.rounds >= min && builder.rounds <= max, + "Rounds value must be between %s and %s (inclusive).", min, max); + this.rounds = builder.rounds; + } + + @Override + protected Map getOptions() { + return ImmutableMap.of("rounds", rounds); + } + + abstract static class Builder { + private int rounds; + + protected abstract T getInstance(); + + /** + * Sets the number of rounds for the hash algorithm. + * + * @param rounds an integer between 0 and 120000 (inclusive). + * @return this builder. + */ + public T setRounds(int rounds) { + this.rounds = rounds; + return getInstance(); + } + + public abstract U build(); + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/Scrypt.java b/src/main/java/com/google/firebase/auth/hash/Scrypt.java new file mode 100644 index 000000000..1c9612285 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/Scrypt.java @@ -0,0 +1,116 @@ +/* + * 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.hash; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.ImmutableMap; +import com.google.common.io.BaseEncoding; +import java.util.Map; + +/** + * Represents the Scrypt password hashing algorithm. This is the + * modified Scrypt algorithm used by + * Firebase Auth. See {@link StandardScrypt} for the standard Scrypt algorithm. Can be used as an + * instance of {@link com.google.firebase.auth.UserImportHash} when importing users. + */ +public final class Scrypt extends RepeatableHash { + + private final String key; + private final String saltSeparator; + private final int memoryCost; + + private Scrypt(Builder builder) { + super("SCRYPT",0, 8, builder); + checkArgument(builder.key != null && builder.key.length > 0, + "A non-empty key is required for Scrypt"); + checkArgument(builder.memoryCost > 0 && builder.memoryCost <= 14, + "memoryCost must be between 1 and 14"); + this.key = BaseEncoding.base64Url().encode(builder.key); + if (builder.saltSeparator != null) { + this.saltSeparator = BaseEncoding.base64Url().encode(builder.saltSeparator); + } else { + this.saltSeparator = BaseEncoding.base64Url().encode(new byte[0]); + } + this.memoryCost = builder.memoryCost; + } + + @Override + protected Map getOptions() { + return ImmutableMap.builder() + .putAll(super.getOptions()) + .put("signerKey", key) + .put("memoryCost", memoryCost) + .put("saltSeparator", saltSeparator) + .build(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends RepeatableHash.Builder { + + private byte[] key; + private byte[] saltSeparator; + private int memoryCost; + + private Builder() {} + + /** + * Sets the signer key. Required field. + * + * @param key Signer key as a byte array. + * @return This builder. + */ + public Builder setKey(byte[] key) { + this.key = key; + return this; + } + + /** + * Sets the salt separator. + * + * @param saltSeparator Salt separator as a byte array. + * @return This builder. + */ + public Builder setSaltSeparator(byte[] saltSeparator) { + this.saltSeparator = saltSeparator; + return this; + } + + /** + * Sets the memory cost. Required field. + * + * @param memoryCost an integer between 1 and 14 (inclusive). + * @return this builder. + */ + public Builder setMemoryCost(int memoryCost) { + this.memoryCost = memoryCost; + return this; + } + + @Override + protected Builder getInstance() { + return this; + } + + public Scrypt build() { + return new Scrypt(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/Sha1.java b/src/main/java/com/google/firebase/auth/hash/Sha1.java new file mode 100644 index 000000000..d14975fda --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/Sha1.java @@ -0,0 +1,46 @@ +/* + * 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.hash; + +/** + * Represents the SHA1 password hashing algorithm. Can be used as an instance of + * {@link com.google.firebase.auth.UserImportHash} when importing users. + */ +public class Sha1 extends RepeatableHash { + + private Sha1(Builder builder) { + super("SHA1", 0, 120000, builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends RepeatableHash.Builder { + + private Builder() {} + + @Override + protected Builder getInstance() { + return this; + } + + public Sha1 build() { + return new Sha1(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/Sha256.java b/src/main/java/com/google/firebase/auth/hash/Sha256.java new file mode 100644 index 000000000..ecc0e7280 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/Sha256.java @@ -0,0 +1,46 @@ +/* + * 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.hash; + +/** + * Represents the SHA256 password hashing algorithm. Can be used as an instance of + * {@link com.google.firebase.auth.UserImportHash} when importing users. + */ +public class Sha256 extends RepeatableHash { + + private Sha256(Builder builder) { + super("SHA256", 0, 120000, builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends RepeatableHash.Builder { + + private Builder() {} + + @Override + protected Builder getInstance() { + return this; + } + + public Sha256 build() { + return new Sha256(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/Sha512.java b/src/main/java/com/google/firebase/auth/hash/Sha512.java new file mode 100644 index 000000000..858d16e05 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/Sha512.java @@ -0,0 +1,46 @@ +/* + * 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.hash; + +/** + * Represents the SHA512 password hashing algorithm. Can be used as an instance of + * {@link com.google.firebase.auth.UserImportHash} when importing users. + */ +public class Sha512 extends RepeatableHash { + + private Sha512(Builder builder) { + super("SHA512", 0, 120000, builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends RepeatableHash.Builder { + + private Builder() {} + + @Override + protected Builder getInstance() { + return this; + } + + public Sha512 build() { + return new Sha512(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java b/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java new file mode 100644 index 000000000..49f7d72f5 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java @@ -0,0 +1,89 @@ +/* + * 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.hash; + +import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.UserImportHash; +import java.util.Map; + +/** + * Represents the Standard Scrypt password hashing algorithm. Can be used as an instance of + * {@link com.google.firebase.auth.UserImportHash} when importing users. + */ +public class StandardScrypt extends UserImportHash { + + private final int derivedKeyLength; + private final int blockSize; + private final int parallelization; + private final int memoryCost; + + private StandardScrypt(Builder builder) { + super("STANDARD_SCRYPT"); + this.derivedKeyLength = builder.derivedKeyLength; + this.blockSize = builder.blockSize; + this.parallelization = builder.parallelization; + this.memoryCost = builder.memoryCost; + } + + @Override + protected Map getOptions() { + return ImmutableMap.of( + "dkLen", derivedKeyLength, + "blockSize", blockSize, + "parallelization", parallelization, + "memoryCost", memoryCost + ); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private int derivedKeyLength; + private int blockSize; + private int parallelization; + private int memoryCost; + + private Builder() {} + + public Builder setDerivedKeyLength(int derivedKeyLength) { + this.derivedKeyLength = derivedKeyLength; + return this; + } + + public Builder setBlockSize(int blockSize) { + this.blockSize = blockSize; + return this; + } + + public Builder setParallelization(int parallelization) { + this.parallelization = parallelization; + return this; + } + + public Builder setMemoryCost(int memoryCost) { + this.memoryCost = memoryCost; + return this; + } + + public StandardScrypt build() { + return new StandardScrypt(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java b/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java new file mode 100644 index 000000000..abf28539d --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java @@ -0,0 +1,49 @@ +/* + * 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.api.client.util.Key; +import java.util.List; + +/** + * Represents the response from identity toolkit for a user import request. + */ +public class UploadAccountResponse { + + @Key("error") + private List errors; + + public List getErrors() { + return errors; + } + + public static class ErrorInfo { + @Key("index") + private int index; + + @Key("message") + private String message; + + public int getIndex() { + return index; + } + + public String getMessage() { + return message; + } + } +} diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 4e71d7f4b..99171d809 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -36,10 +36,13 @@ import com.google.api.core.ApiFutureCallback; import com.google.api.core.ApiFutures; 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.auth.UserRecord.CreateRequest; import com.google.firebase.auth.UserRecord.UpdateRequest; +import com.google.firebase.auth.hash.Scrypt; import com.google.firebase.testing.IntegrationTestUtils; import java.io.IOException; import java.util.ArrayList; @@ -60,6 +63,8 @@ public class FirebaseAuthIT { private static final String ID_TOOLKIT_URL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken"; + private static final String ID_TOOLKIT_URL2 = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword"; private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); private static final HttpTransport transport = Utils.getDefaultTransport(); @@ -433,6 +438,66 @@ public void testCustomTokenWithClaims() throws Exception { assertEquals("silver", decoded.getClaims().get("subscription")); } + @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(); + ImportUserRecord user = ImportUserRecord.builder() + .setUid(randomId) + .setEmail(userEmail) + .build(); + + UserImportResult result = auth.importUsersAsync(ImmutableList.of(user)).get(); + assertEquals(1, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + + try { + UserRecord savedUser = auth.getUserAsync(randomId).get(); + assertEquals(userEmail, savedUser.getEmail()); + } finally { + auth.deleteUserAsync(randomId).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(); + final byte[] passwordHash = BaseEncoding.base64().decode( + "V358E8LdWJXAO7muq0CufVpEOXaj8aFiC7T/rcaGieN04q/ZPJ08WhJEHGjj9lz/2TT+/86N5VjVoc5DdBhBiw=="); + ImportUserRecord user = ImportUserRecord.builder() + .setUid(randomId) + .setEmail(userEmail) + .setPasswordHash(passwordHash) + .setPasswordSalt("NaCl".getBytes()) + .build(); + + final byte[] scryptKey = BaseEncoding.base64().decode( + "jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZJzxUECKbm0QsEmYUSDzZvpjeJ9WmXA=="); + final byte[] saltSeparator = BaseEncoding.base64().decode("Bw=="); + UserImportResult result = auth.importUsersAsync( + ImmutableList.of(user), + UserImportOptions.withHash(Scrypt.builder() + .setKey(scryptKey) + .setSaltSeparator(saltSeparator) + .setRounds(8) + .setMemoryCost(14) + .build())).get(); + assertEquals(1, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + + try { + UserRecord savedUser = auth.getUserAsync(randomId).get(); + assertEquals(userEmail, savedUser.getEmail()); + String idToken = signInWithPassword(userEmail, "password"); + assertFalse(Strings.isNullOrEmpty(idToken)); + } finally { + auth.deleteUserAsync(randomId).get(); + } + } + private String signInWithCustomToken(String customToken) throws IOException { GenericUrl url = new GenericUrl(ID_TOOLKIT_URL + "?key=" + IntegrationTestUtils.getApiKey()); @@ -450,6 +515,23 @@ private String signInWithCustomToken(String customToken) throws IOException { } } + private String signInWithPassword(String email, String password) throws IOException { + GenericUrl url = new GenericUrl(ID_TOOLKIT_URL2 + "?key=" + + IntegrationTestUtils.getApiKey()); + Map content = ImmutableMap.of( + "email", email, "password", password); + 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(); diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index b10058420..df5a34e2d 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -252,6 +252,159 @@ public void testDeleteUser() throws Exception { checkRequestHeaders(interceptor); } + @Test + public void testImportUsers() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + ImportUserRecord user1 = ImportUserRecord.builder().setUid("user1").build(); + ImportUserRecord user2 = ImportUserRecord.builder().setUid("user2").build(); + + List users = ImmutableList.of(user1, user2); + UserImportResult result = FirebaseAuth.getInstance().importUsersAsync(users, null).get(); + checkRequestHeaders(interceptor); + assertEquals(2, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + assertTrue(result.getErrors().isEmpty()); + + 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(1, parsed.size()); + List> expected = ImmutableList.of( + user1.getProperties(jsonFactory), + user2.getProperties(jsonFactory) + ); + assertEquals(expected, parsed.get("users")); + } + + @Test + public void testImportUsersError() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("importUsersError.json")); + ImportUserRecord user1 = ImportUserRecord.builder() + .setUid("user1") + .build(); + ImportUserRecord user2 = ImportUserRecord.builder() + .setUid("user2") + .build(); + ImportUserRecord user3 = ImportUserRecord.builder() + .setUid("user3") + .build(); + + List users = ImmutableList.of(user1, user2, user3); + UserImportResult result = FirebaseAuth.getInstance().importUsersAsync(users, null).get(); + checkRequestHeaders(interceptor); + assertEquals(1, result.getSuccessCount()); + assertEquals(2, result.getFailureCount()); + assertEquals(2, result.getErrors().size()); + + ErrorInfo error = result.getErrors().get(0); + assertEquals(0, error.getIndex()); + assertEquals("Some error occurred in user1", error.getReason()); + error = result.getErrors().get(1); + assertEquals(2, error.getIndex()); + assertEquals("Another error occurred in user3", error.getReason()); + + 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(1, parsed.size()); + List> expected = ImmutableList.of( + user1.getProperties(jsonFactory), + user2.getProperties(jsonFactory), + user3.getProperties(jsonFactory) + ); + assertEquals(expected, parsed.get("users")); + } + + @Test + public void testImportUsersWithHash() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + ImportUserRecord user1 = ImportUserRecord.builder() + .setUid("user1") + .build(); + ImportUserRecord user2 = ImportUserRecord.builder() + .setUid("user2") + .setPasswordHash("password".getBytes()) + .build(); + + List users = ImmutableList.of(user1, user2); + UserImportHash hash = new UserImportHash("MOCK_HASH") { + @Override + protected Map getOptions() { + return ImmutableMap.of("key1", "value1", "key2", true); + } + }; + UserImportResult result = FirebaseAuth.getInstance().importUsersAsync(users, + UserImportOptions.withHash(hash)).get(); + checkRequestHeaders(interceptor); + assertEquals(2, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + assertTrue(result.getErrors().isEmpty()); + + 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(4, parsed.size()); + List> expected = ImmutableList.of( + user1.getProperties(jsonFactory), + user2.getProperties(jsonFactory) + ); + assertEquals(expected, parsed.get("users")); + assertEquals("MOCK_HASH", parsed.get("hashAlgorithm")); + assertEquals("value1", parsed.get("key1")); + assertEquals(Boolean.TRUE, parsed.get("key2")); + } + + @Test + public void testImportUsersMissingHash() { + initializeAppForUserManagement(); + ImportUserRecord user1 = ImportUserRecord.builder() + .setUid("user1") + .build(); + ImportUserRecord user2 = ImportUserRecord.builder() + .setUid("user2") + .setPasswordHash("password".getBytes()) + .build(); + + List users = ImmutableList.of(user1, user2); + try { + FirebaseAuth.getInstance().importUsersAsync(users); + fail("No error thrown for missing hash option"); + } catch (IllegalArgumentException expected) { + assertEquals("UserImportHash option is required when at least one user has a password. " + + "Provide a UserImportHash via UserImportOptions.withHash().", expected.getMessage()); + } + } + + @Test + public void testImportUsersEmptyList() { + initializeAppForUserManagement(); + try { + FirebaseAuth.getInstance().importUsersAsync(ImmutableList.of()); + fail("No error thrown for empty user list"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testImportUsersLargeList() { + initializeAppForUserManagement(); + ImmutableList.Builder users = ImmutableList.builder(); + for (int i = 0; i < 1001; i++) { + users.add(ImportUserRecord.builder().setUid("test" + i).build()); + } + try { + FirebaseAuth.getInstance().importUsersAsync(users.build()); + fail("No error thrown for large list"); + } catch (IllegalArgumentException expected) { + // expected + } + } + @Test public void testCreateSessionCookie() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement( @@ -267,12 +420,13 @@ public void testCreateSessionCookie() throws Exception { interceptor.getResponse().getRequest().getContent().writeTo(out); JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + assertEquals(2, parsed.size()); assertEquals("testToken", parsed.get("idToken")); assertEquals(new BigDecimal(3600), parsed.get("validDuration")); } @Test - public void testCreateSessionInvalidArguments() { + public void testCreateSessionCookieInvalidArguments() { FirebaseApp.initializeApp(new FirebaseOptions.Builder() .setCredentials(credentials) .build()); diff --git a/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java b/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java new file mode 100644 index 000000000..011a5cc04 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/ImportUserRecordTest.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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +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 java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +public class ImportUserRecordTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + @Test + public void testUidOnlyRecord() { + ImportUserRecord record = ImportUserRecord.builder() + .setUid("testuid") + .build(); + assertEquals(ImmutableMap.of("localId", "testuid"), record.getProperties(JSON_FACTORY)); + } + + @Test + public void testAllProperties() throws IOException { + Date date = new Date(); + UserProvider provider1 = UserProvider.builder() + .setUid("testuid") + .setProviderId("google.com") + .build(); + UserProvider provider2 = UserProvider.builder() + .setUid("testuid") + .setProviderId("test.com") + .build(); + ImportUserRecord record = ImportUserRecord.builder() + .setUid("testuid") + .setEmail("test@example.com") + .setDisplayName("Test User") + .setPhotoUrl("https://test.com/user.png") + .setPhoneNumber("+1234567890") + .setUserMetadata(new UserMetadata(date.getTime(), date.getTime())) + .setDisabled(false) + .setEmailVerified(true) + .setPasswordHash("password".getBytes()) + .setPasswordSalt("salt".getBytes()) + .addUserProvider(provider1) + .addAllUserProviders(ImmutableList.of(provider2)) + .putCustomClaim("admin", true) + .putAllCustomClaims(ImmutableMap.of("package", "gold")) + .build(); + + Map properties = record.getProperties(JSON_FACTORY); + + Map customClaims = new HashMap<>(); + String customAttributes = (String) properties.get("customAttributes"); + JSON_FACTORY.createJsonParser(customAttributes).parse(customClaims); + assertEquals(ImmutableMap.of("admin", true, "package", "gold"), customClaims); + + Map expected = ImmutableMap.builder() + .put("localId", "testuid") + .put("email", "test@example.com") + .put("displayName", "Test User") + .put("photoUrl", "https://test.com/user.png") + .put("phoneNumber", "+1234567890") + .put("createdAt", date.getTime()) + .put("lastLoginAt", date.getTime()) + .put("disabled", false) + .put("emailVerified", true) + .put("passwordHash", BaseEncoding.base64Url().encode("password".getBytes())) + .put("salt", BaseEncoding.base64Url().encode("salt".getBytes())) + .put("providerUserInfo", ImmutableList.of(provider1, provider2)) + .put("customAttributes", customAttributes) + .build(); + assertEquals(expected, properties); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidUid() { + ImportUserRecord.builder() + .setUid(Strings.repeat("a", 129)) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidEmail() { + ImportUserRecord.builder() + .setUid("test") + .setEmail("not-an-email") + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidPhotoUrl() { + ImportUserRecord.builder() + .setUid("test") + .setPhotoUrl("not a url") + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidPhoneNumber() { + ImportUserRecord.builder() + .setUid("test") + .setPhoneNumber("not a phone number") + .build(); + } + + @Test + public void testNullUserProvider() { + try { + ImportUserRecord.builder() + .setUid("test") + .addUserProvider(null).build(); + fail("No error thrown for null provider"); + } catch (NullPointerException expected) { + // expected + } + + try { + List providers = new ArrayList<>(); + providers.add(null); + ImportUserRecord.builder() + .setUid("test") + .addAllUserProviders(providers).build(); + fail("No error thrown for null provider"); + } catch (NullPointerException expected) { + // expected + } + } + + @Test + public void testNullOrEmptyCustomClaims() { + try { + ImportUserRecord.builder() + .setUid("test") + .putCustomClaim("foo", null).build(); + fail("No error thrown for null claim value"); + } catch (NullPointerException expected) { + // expected + } + + try { + ImportUserRecord.builder() + .setUid("test") + .putCustomClaim(null, "foo").build(); + fail("No error thrown for null claim name"); + } catch (NullPointerException expected) { + // expected + } + + try { + ImportUserRecord.builder() + .setUid("test") + .putCustomClaim("", "foo").build(); + fail("No error thrown for empty claim name"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testReservedClaims() { + for (String key : FirebaseUserManager.RESERVED_CLAIMS) { + try { + ImportUserRecord.builder() + .setUid("test") + .putCustomClaim(key, "foo").build(); + fail("No error thrown for reserved claim"); + } catch (IllegalArgumentException expected) { + // expected + } + } + } + + @Test + public void testLargeCustomClaims() { + ImportUserRecord user = ImportUserRecord.builder() + .setUid("test") + .putCustomClaim("foo", Strings.repeat("a", 1000)) + .build(); + try { + user.getProperties(JSON_FACTORY); + fail("No error thrown for large claim value"); + } catch (IllegalArgumentException expected) { + // expected + } + } +} diff --git a/src/test/java/com/google/firebase/auth/UserImportHashTest.java b/src/test/java/com/google/firebase/auth/UserImportHashTest.java new file mode 100644 index 000000000..e4a3af14a --- /dev/null +++ b/src/test/java/com/google/firebase/auth/UserImportHashTest.java @@ -0,0 +1,154 @@ +/* + * 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 com.google.common.io.BaseEncoding; +import com.google.firebase.auth.hash.Bcrypt; +import com.google.firebase.auth.hash.HmacMd5; +import com.google.firebase.auth.hash.HmacSha1; +import com.google.firebase.auth.hash.HmacSha256; +import com.google.firebase.auth.hash.HmacSha512; +import com.google.firebase.auth.hash.Md5; +import com.google.firebase.auth.hash.Pbkdf2Sha256; +import com.google.firebase.auth.hash.PbkdfSha1; +import com.google.firebase.auth.hash.Scrypt; +import com.google.firebase.auth.hash.Sha1; +import com.google.firebase.auth.hash.Sha256; +import com.google.firebase.auth.hash.Sha512; +import com.google.firebase.auth.hash.StandardScrypt; +import java.util.Map; +import org.junit.Test; + +public class UserImportHashTest { + + private static final byte[] SIGNER_KEY = "key".getBytes(); + private static final byte[] SALT_SEPARATOR = "separator".getBytes(); + + private static class MockHash extends UserImportHash { + MockHash() { + super("MOCK_HASH"); + } + + @Override + protected Map getOptions() { + return ImmutableMap.of("key", "value"); + } + } + + @Test + public void testBase() { + UserImportHash hash = new MockHash(); + assertEquals(ImmutableMap.of("hashAlgorithm", "MOCK_HASH", "key", "value"), + hash.getProperties()); + } + + @Test + public void testScryptHash() { + UserImportHash scrypt = Scrypt.builder() + .setKey(SIGNER_KEY) + .setSaltSeparator(SALT_SEPARATOR) + .setRounds(8) + .setMemoryCost(14) + .build(); + Map properties = ImmutableMap.of( + "hashAlgorithm", "SCRYPT", + "signerKey", BaseEncoding.base64Url().encode(SIGNER_KEY), + "saltSeparator", BaseEncoding.base64Url().encode(SALT_SEPARATOR), + "rounds", 8, + "memoryCost", 14 + ); + assertEquals(properties, scrypt.getProperties()); + + scrypt = Scrypt.builder() + .setKey(SIGNER_KEY) + .setRounds(8) + .setMemoryCost(14) + .build(); + properties = ImmutableMap.of( + "hashAlgorithm", "SCRYPT", + "signerKey", BaseEncoding.base64Url().encode(SIGNER_KEY), + "saltSeparator", "", + "rounds", 8, + "memoryCost", 14 + ); + assertEquals(properties, scrypt.getProperties()); + } + + @Test + public void testStandardScryptHash() { + UserImportHash scrypt = StandardScrypt.builder() + .setBlockSize(1) + .setParallelization(2) + .setDerivedKeyLength(3) + .setMemoryCost(4) + .build(); + Map properties = ImmutableMap.of( + "hashAlgorithm", "STANDARD_SCRYPT", + "blockSize", 1, + "parallelization", 2, + "dkLen", 3, + "memoryCost", 4 + ); + assertEquals(properties, scrypt.getProperties()); + } + + @Test + public void testHmacHash() { + Map hashes = ImmutableMap.of( + "HMAC_SHA512", HmacSha512.builder().setKey(SIGNER_KEY).build(), + "HMAC_SHA256", HmacSha256.builder().setKey(SIGNER_KEY).build(), + "HMAC_SHA1", HmacSha1.builder().setKey(SIGNER_KEY).build(), + "HMAC_MD5", HmacMd5.builder().setKey(SIGNER_KEY).build() + ); + for (Map.Entry entry : hashes.entrySet()) { + Map properties = ImmutableMap.of( + "hashAlgorithm", entry.getKey(), + "signerKey", BaseEncoding.base64Url().encode(SIGNER_KEY) + ); + assertEquals(properties, entry.getValue().getProperties()); + } + } + + @Test + public void testBasicHash() { + Map hashes = ImmutableMap.builder() + .put("SHA512", Sha512.builder().setRounds(42).build()) + .put("SHA256", Sha256.builder().setRounds(42).build()) + .put("SHA1", Sha1.builder().setRounds(42).build()) + .put("MD5", Md5.builder().setRounds(42).build()) + .put("PBKDF2_SHA256", Pbkdf2Sha256.builder().setRounds(42).build()) + .put("PBKDF_SHA1", PbkdfSha1.builder().setRounds(42).build()) + .build(); + for (Map.Entry entry : hashes.entrySet()) { + Map properties = ImmutableMap.of( + "hashAlgorithm", entry.getKey(), + "rounds", 42 + ); + assertEquals(properties, entry.getValue().getProperties()); + } + } + + @Test + public void testBcryptHash() { + UserImportHash bcrypt = Bcrypt.getInstance(); + Map properties = ImmutableMap.of("hashAlgorithm", "BCRYPT"); + assertEquals(properties, bcrypt.getProperties()); + } +} diff --git a/src/test/java/com/google/firebase/auth/UserImportOptionsTest.java b/src/test/java/com/google/firebase/auth/UserImportOptionsTest.java new file mode 100644 index 000000000..7913997b8 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/UserImportOptionsTest.java @@ -0,0 +1,50 @@ +/* + * 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.assertSame; + +import com.google.common.collect.ImmutableMap; +import com.google.common.io.BaseEncoding; +import com.google.firebase.auth.hash.HmacSha512; +import java.util.Map; +import org.junit.Test; + +public class UserImportOptionsTest { + + @Test(expected = NullPointerException.class) + public void testEmptyOptions() { + UserImportOptions.builder().build(); + } + + @Test + public void testHash() { + HmacSha512 hash = HmacSha512.builder() + .setKey("key".getBytes()) + .build(); + UserImportOptions options = UserImportOptions.builder() + .setHash(hash) + .build(); + Map expected = ImmutableMap.of( + "hashAlgorithm", "HMAC_SHA512", + "signerKey", BaseEncoding.base64Url().encode("key".getBytes()) + ); + assertEquals(expected, options.getProperties()); + assertSame(hash, options.getHash()); + } +} diff --git a/src/test/java/com/google/firebase/auth/UserProviderTest.java b/src/test/java/com/google/firebase/auth/UserProviderTest.java new file mode 100644 index 000000000..69806dc36 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/UserProviderTest.java @@ -0,0 +1,84 @@ +/* + * 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.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; + +public class UserProviderTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + @Test + public void testAllProperties() throws IOException { + UserProvider provider = UserProvider.builder() + .setUid("testuid") + .setProviderId("google.com") + .setEmail("test@example.com") + .setDisplayName("Test User") + .setPhotoUrl("https://test.com/user.png") + .build(); + String json = JSON_FACTORY.toString(provider); + Map parsed = new HashMap<>(); + JSON_FACTORY.createJsonParser(json).parse(parsed); + Map expected = ImmutableMap.of( + "rawId", "testuid", + "providerId", "google.com", + "email", "test@example.com", + "displayName", "Test User", + "photoUrl", "https://test.com/user.png" + ); + assertEquals(expected, parsed); + } + + @Test + public void testRequiredProperties() throws IOException { + UserProvider provider = UserProvider.builder() + .setUid("testuid") + .setProviderId("google.com") + .build(); + String json = JSON_FACTORY.toString(provider); + Map parsed = new HashMap<>(); + JSON_FACTORY.createJsonParser(json).parse(parsed); + Map expected = ImmutableMap.of( + "rawId", "testuid", + "providerId", "google.com" + ); + assertEquals(expected, parsed); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoUid() { + UserProvider.builder() + .setProviderId("google.com") + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoProviderId() { + UserProvider.builder() + .setUid("testuid") + .build(); + } +} diff --git a/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java b/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java new file mode 100644 index 000000000..aabe4444c --- /dev/null +++ b/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java @@ -0,0 +1,107 @@ +/* + * 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.hash; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.Test; + +public class InvalidHashTest { + + private static final byte[] SIGNER_KEY = "key".getBytes(); + private static final byte[] SALT_SEPARATOR = "separator".getBytes(); + + @Test + public void testInvalidHmac() { + List builders = ImmutableList.of( + HmacSha512.builder(), + HmacSha256.builder(), + HmacSha1.builder(), + HmacMd5.builder() + ); + for (Hmac.Builder builder : builders) { + try { + builder.build(); + fail("No error thrown for missing key"); + } catch (Exception expected) { + assertTrue(expected instanceof IllegalArgumentException); + } + } + } + + @Test + public void testInvalidRepeatableHash() { + List builders = ImmutableList.builder() + .add(Sha512.builder().setRounds(-1)) + .add(Sha256.builder().setRounds(-1)) + .add(Sha1.builder().setRounds(-1)) + .add(Md5.builder().setRounds(-1)) + .add(Pbkdf2Sha256.builder().setRounds(-1)) + .add(PbkdfSha1.builder().setRounds(-1)) + .add(Sha512.builder().setRounds(120001)) + .add(Sha256.builder().setRounds(120001)) + .add(Sha1.builder().setRounds(120001)) + .add(Md5.builder().setRounds(120001)) + .add(Pbkdf2Sha256.builder().setRounds(120001)) + .add(PbkdfSha1.builder().setRounds(120001)) + .build(); + for (RepeatableHash.Builder builder : builders) { + try { + builder.build(); + fail("No error thrown for invalid rounds"); + } catch (Exception expected) { + assertTrue(expected instanceof IllegalArgumentException); + } + } + } + + @Test + public void testInvalidScrypt() { + List builders = ImmutableList.of( + Scrypt.builder() // missing signer key + .setSaltSeparator(SALT_SEPARATOR) + .setRounds(5) + .setMemoryCost(12), + Scrypt.builder() // invalid rounds (> 8) + .setKey(SIGNER_KEY) + .setSaltSeparator(SALT_SEPARATOR) + .setRounds(9) + .setMemoryCost(14), + Scrypt.builder() // invalid rounds (< 0) + .setKey(SIGNER_KEY) + .setSaltSeparator(SALT_SEPARATOR) + .setRounds(-1) + .setMemoryCost(14), + Scrypt.builder() // invalid memory cost (> 15) + .setKey(SIGNER_KEY) + .setSaltSeparator(SALT_SEPARATOR) + .setRounds(8) + .setMemoryCost(15) + ); + for (Scrypt.Builder builder : builders) { + try { + builder.build(); + fail("No error thrown for invalid configuration"); + } catch (IllegalArgumentException expected) { + // expected + } + } + } +} diff --git a/src/test/resources/importUsersError.json b/src/test/resources/importUsersError.json new file mode 100644 index 000000000..9233f8a34 --- /dev/null +++ b/src/test/resources/importUsersError.json @@ -0,0 +1,6 @@ +{ + "error": [ + {"index": 0, "message": "Some error occurred in user1"}, + {"index": 2, "message": "Another error occurred in user3"} + ] +} From 267d92f9c90530c096c0557211460d6b162417a4 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 29 May 2018 15:36:00 -0700 Subject: [PATCH 016/456] 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 017/456] 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 018/456] 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 019/456] 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 020/456] 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 021/456] 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 022/456] 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 023/456] 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 024/456] 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 025/456] 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 026/456] 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 027/456] 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 028/456] 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 029/456] 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 030/456] 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 031/456] 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 032/456] 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 033/456] 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 034/456] 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 035/456] 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 036/456] 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 037/456] 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 038/456] 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 039/456] 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 040/456] 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 041/456] 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 042/456] 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 043/456] 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 044/456] 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: + * + *

      + *
    • When the link is handled in the web action widgets, this is the deep link in the + * {@code continueUrl} query parameter.
    • + *
    • When the link is handled in the app directly, this is the {@code continueUrl} query + * parameter in the deep link of the Dynamic Link.
    • + *
    + * + *

    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 045/456] 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 046/456] 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 047/456] 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: * *

      *
    • When the link is handled in the web action widgets, this is the deep link in the diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index ec1d622f5..843c42f1f 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -988,9 +988,9 @@ public String generatePasswordResetLink(@NonNull String email) throws FirebaseAu * address. * * @param email The email of the user whose password is to be reset. - * @param settings The action code settings which defines whether + * @param settings The action code settings object 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. + * passed in the deep link. * @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. @@ -1019,9 +1019,9 @@ public ApiFuture generatePasswordResetLinkAsync(@NonNull String email) { * operation asynchronously. * * @param email The email of the user whose password is to be reset. - * @param settings The action code settings which defines whether + * @param settings The action code settings object 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. + * passed in the deep link. * @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}. @@ -1079,9 +1079,9 @@ public ApiFuture generateEmailVerificationLinkAsync(@NonNull String emai * operation asynchronously. * * @param email The email of the user to be verified. - * @param settings The action code settings which defines whether + * @param settings The action code settings object 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. + * passed in the deep link. * @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}. @@ -1098,9 +1098,9 @@ public ApiFuture generateEmailVerificationLinkAsync( * code settings provided. * * @param email The email of the user signing in. - * @param settings The action code settings which defines whether + * @param settings The action code settings object 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. + * passed in the deep link. * @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. @@ -1115,9 +1115,9 @@ public String generateSignInWithEmailLink( * operation asynchronously. * * @param email The email of the user signing in. - * @param settings The action code settings which defines whether + * @param settings The action code settings object 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. + * passed in the deep link. * @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}. From 6b46deda57c18c534f2bc261b0927a52d4b89ba2 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 9 Jan 2019 13:38:53 -0800 Subject: [PATCH 048/456] Staging Release 6.7.0 (#241) * Updating CHANGELOG for 6.7.0 release. * [maven-release-plugin] prepare release v6.7.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 0f9deac51..df571de79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +- + +# v6.7.0 + - [added] Added `generatePasswordResetLink()`, `generateEmailVerificationLink()` and `generateSignInWithEmailLink()` methods to the `FirebaseAuth` API. - `Aps` class now supports configuring a critical sound. A new diff --git a/pom.xml b/pom.xml index fbd144d03..df453ac71 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.6.1-SNAPSHOT + 6.7.1-SNAPSHOT jar firebase-admin From fd63d97bf854499987398a5c7a06405d0768a95b Mon Sep 17 00:00:00 2001 From: Matej Spiller Muys Date: Thu, 17 Jan 2019 22:50:11 +0100 Subject: [PATCH 049/456] Remove Dependency on org.json:json Artifact (#113), Java 11 build problems (#243) (#242) * Fix Java 11 build problems (#243) * Remove Dependency on org.json:json Artifact (#113) --- CHANGELOG.md | 3 +- pom.xml | 7 +- .../firebase/database/util/JsonMapper.java | 138 ++++++++---------- .../connection/WebsocketConnectionTest.java | 4 +- .../core/JvmAuthTokenProviderTest.java | 14 +- .../database/util/JsonMapperTest.java | 34 ++--- 6 files changed, 79 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df571de79..7763b857f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased -- +- [changed] Removed org.json dependency and replaced with com.google.code.gson. +- [changed] Upgraded Mockito dependency, and fixed the build on Java 11. # v6.7.0 diff --git a/pom.xml b/pom.xml index df453ac71..0fcfe205a 100644 --- a/pom.xml +++ b/pom.xml @@ -437,11 +437,6 @@ guava 20.0 - - org.json - json - 20160810 - org.slf4j slf4j-api @@ -467,7 +462,7 @@ org.mockito mockito-core - 2.7.21 + 2.23.4 test diff --git a/src/main/java/com/google/firebase/database/util/JsonMapper.java b/src/main/java/com/google/firebase/database/util/JsonMapper.java index 0c178f7e1..0321c8bb2 100644 --- a/src/main/java/com/google/firebase/database/util/JsonMapper.java +++ b/src/main/java/com/google/firebase/database/util/JsonMapper.java @@ -16,20 +16,18 @@ package com.google.firebase.database.util; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; import java.io.IOException; +import java.io.StringReader; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.json.JSONStringer; -import org.json.JSONTokener; - /** * Helper class to convert from/to JSON strings. TODO: This class should ideally not live in * firebase-database-connection, but it's required by both firebase-database and @@ -37,102 +35,82 @@ */ public class JsonMapper { - public static String serializeJson(Map object) throws IOException { - return serializeJsonValue(object); - } - - @SuppressWarnings("unchecked") - public static String serializeJsonValue(Object object) throws IOException { - if (object == null) { - return "null"; - } else if (object instanceof String) { - return JSONObject.quote((String) object); - } else if (object instanceof Number) { - try { - return JSONObject.numberToString((Number) object); - } catch (JSONException e) { - throw new IOException("Could not serialize number", e); - } - } else if (object instanceof Boolean) { - return ((Boolean) object) ? "true" : "false"; - } else { - try { - JSONStringer stringer = new JSONStringer(); - serializeJsonValue(object, stringer); - return stringer.toString(); - } catch (JSONException e) { - throw new IOException("Failed to serialize JSON", e); - } - } - } + private static final Gson GSON = new GsonBuilder().serializeNulls().create(); - private static void serializeJsonValue(Object object, JSONStringer stringer) - throws IOException, JSONException { - if (object instanceof Map) { - stringer.object(); - @SuppressWarnings("unchecked") - Map map = (Map) object; - for (Map.Entry entry : map.entrySet()) { - stringer.key(entry.getKey()); - serializeJsonValue(entry.getValue(), stringer); - } - stringer.endObject(); - } else if (object instanceof Collection) { - Collection collection = (Collection) object; - stringer.array(); - for (Object entry : collection) { - serializeJsonValue(entry, stringer); - } - stringer.endArray(); - } else { - stringer.value(object); + public static String serializeJson(Object object) throws IOException { + try { + return GSON.toJson(object); + } catch (JsonSyntaxException e) { + throw new IOException(e); } } public static Map parseJson(String json) throws IOException { try { - return unwrapJsonObject(new JSONObject(json)); - } catch (JSONException e) { + JsonReader jsonReader = new JsonReader(new StringReader(json)); + return unwrapJsonObject(jsonReader); + } catch (IllegalStateException | JsonSyntaxException e) { throw new IOException(e); } } public static Object parseJsonValue(String json) throws IOException { try { - return unwrapJson(new JSONTokener(json).nextValue()); - } catch (JSONException e) { + JsonReader jsonReader = new JsonReader(new StringReader(json)); + jsonReader.setLenient(true); + return unwrapJson(jsonReader); + } catch (IllegalStateException | JsonSyntaxException e) { throw new IOException(e); } } - @SuppressWarnings("unchecked") - private static Map unwrapJsonObject(JSONObject jsonObject) throws JSONException { - Map map = new HashMap<>(jsonObject.length()); - Iterator keys = jsonObject.keys(); - while (keys.hasNext()) { - String key = keys.next(); - map.put(key, unwrapJson(jsonObject.get(key))); + private static Map unwrapJsonObject(JsonReader jsonReader) throws IOException { + Map map = new HashMap<>(); + jsonReader.beginObject(); + while (jsonReader.peek() != JsonToken.END_OBJECT) { + String key = jsonReader.nextName(); + map.put(key, unwrapJson(jsonReader)); } + jsonReader.endObject(); return map; } - private static List unwrapJsonArray(JSONArray jsonArray) throws JSONException { - List list = new ArrayList<>(jsonArray.length()); - for (int i = 0; i < jsonArray.length(); i++) { - list.add(unwrapJson(jsonArray.get(i))); + private static List unwrapJsonArray(JsonReader jsonReader) throws IOException { + List list = new ArrayList<>(); + jsonReader.beginArray(); + while (jsonReader.peek() != JsonToken.END_ARRAY) { + list.add(unwrapJson(jsonReader)); } + jsonReader.endArray(); return list; } - private static Object unwrapJson(Object o) throws JSONException { - if (o instanceof JSONObject) { - return unwrapJsonObject((JSONObject) o); - } else if (o instanceof JSONArray) { - return unwrapJsonArray((JSONArray) o); - } else if (o.equals(JSONObject.NULL)) { - return null; - } else { - return o; + private static Object unwrapJson(JsonReader jsonReader) throws IOException { + switch (jsonReader.peek()) { + case BEGIN_ARRAY: + return unwrapJsonArray(jsonReader); + case BEGIN_OBJECT: + return unwrapJsonObject(jsonReader); + case STRING: + return jsonReader.nextString(); + case NUMBER: + String value = jsonReader.nextString(); + if (value.matches("-?\\d+")) { + long longValue = Long.parseLong(value); + if (longValue <= Integer.MAX_VALUE && longValue >= Integer.MIN_VALUE) { + return (int) longValue; + } + return Long.valueOf(value); + } + return Double.parseDouble(value); + case BOOLEAN: + return jsonReader.nextBoolean(); + case NULL: + jsonReader.nextNull(); + return null; + default: + throw new IllegalStateException("unknown type " + jsonReader.peek()); } } + } diff --git a/src/test/java/com/google/firebase/database/connection/WebsocketConnectionTest.java b/src/test/java/com/google/firebase/database/connection/WebsocketConnectionTest.java index 896d4fe63..02f2bfe50 100644 --- a/src/test/java/com/google/firebase/database/connection/WebsocketConnectionTest.java +++ b/src/test/java/com/google/firebase/database/connection/WebsocketConnectionTest.java @@ -112,9 +112,7 @@ public void testReceiveMultipleFrames() { public void testIncomingMessageIOError() { MockClientFactory clientFactory = new MockClientFactory(); ImmutableMap data = ImmutableMap.of("key", "value"); - Mockito.doThrow(IOException.class).when(clientFactory.delegate).onMessage(data); - clientFactory.eventHandler.onMessage("{\"key\":\"value\"}"); - Mockito.verify(clientFactory.delegate, Mockito.times(1)).onMessage(data); + clientFactory.eventHandler.onMessage("ERR{\"key\":\"value\"}"); Mockito.verify(clientFactory.client, Mockito.times(1)).close(); Mockito.verify(clientFactory.delegate, Mockito.times(1)).onDisconnect(false); } diff --git a/src/test/java/com/google/firebase/database/core/JvmAuthTokenProviderTest.java b/src/test/java/com/google/firebase/database/core/JvmAuthTokenProviderTest.java index a174b3ed9..7b8fe5229 100644 --- a/src/test/java/com/google/firebase/database/core/JvmAuthTokenProviderTest.java +++ b/src/test/java/com/google/firebase/database/core/JvmAuthTokenProviderTest.java @@ -30,6 +30,7 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.database.util.JsonMapper; import com.google.firebase.testing.TestUtils; import java.io.IOException; @@ -46,7 +47,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import org.json.JSONObject; import org.junit.After; import org.junit.Test; @@ -100,7 +100,7 @@ public void testGetTokenNoRefresh() throws IOException, InterruptedException { } @Test - public void testGetTokenWithAuthOverrides() throws InterruptedException { + public void testGetTokenWithAuthOverrides() throws InterruptedException, IOException { MockGoogleCredentials credentials = new MockGoogleCredentials("mock-token"); Map auth = ImmutableMap.of("uid", "test"); FirebaseOptions options = new FirebaseOptions.Builder() @@ -226,13 +226,15 @@ public void onChanged(OAuth2Credentials credentials) throws IOException { assertTrue(semaphore.tryAcquire(TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); } - private void assertToken(String token, String expectedToken, Map expectedAuth) { + private void assertToken(String token, String expectedToken, Map expectedAuth) + throws IOException { assertTrue(token.startsWith("gauth|")); String jsonString = token.substring(6); - JSONObject json = new JSONObject(jsonString); - assertEquals(expectedToken, json.getString("token")); + Map map = JsonMapper.parseJson(jsonString); - Map auth = json.getJSONObject("auth").toMap(); + assertEquals(expectedToken, map.get("token")); + + Map auth = (Map)map.get("auth"); DeepEquals.deepEquals(expectedAuth, auth); } diff --git a/src/test/java/com/google/firebase/database/util/JsonMapperTest.java b/src/test/java/com/google/firebase/database/util/JsonMapperTest.java index ceecfe155..0134d2733 100644 --- a/src/test/java/com/google/firebase/database/util/JsonMapperTest.java +++ b/src/test/java/com/google/firebase/database/util/JsonMapperTest.java @@ -17,7 +17,6 @@ package com.google.firebase.database.util; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; @@ -28,55 +27,42 @@ import java.util.List; import java.util.Map; -import org.json.JSONException; -import org.junit.Ignore; import org.junit.Test; public class JsonMapperTest { @Test public void testNull() throws IOException { - assertEquals("null", JsonMapper.serializeJsonValue(null)); + assertEquals("null", JsonMapper.serializeJson(null)); } @Test public void testString() throws IOException { - assertEquals("\"foo\"", JsonMapper.serializeJsonValue("foo")); + assertEquals("\"foo\"", JsonMapper.serializeJson("foo")); } @Test public void testBoolean() throws IOException { - assertEquals("true", JsonMapper.serializeJsonValue(true)); - assertEquals("false", JsonMapper.serializeJsonValue(false)); + assertEquals("true", JsonMapper.serializeJson(true)); + assertEquals("false", JsonMapper.serializeJson(false)); } @Test public void testMap() throws IOException { - assertEquals("{\"foo\":\"bar\"}", JsonMapper.serializeJsonValue(ImmutableMap.of("foo", "bar"))); + assertEquals("{\"foo\":\"bar\"}", JsonMapper.serializeJson(ImmutableMap.of("foo", "bar"))); } @Test public void testList() throws IOException { - assertEquals("[\"foo\",\"bar\"]", JsonMapper.serializeJsonValue( + assertEquals("[\"foo\",\"bar\"]", JsonMapper.serializeJson( ImmutableList.of("foo", "bar"))); } - @Test - public void testInvalidObject() { - try { - JsonMapper.serializeJsonValue(new Object()); - fail("No error thrown for invalid object"); - } catch (IOException expected) { - // expected - assertTrue(expected.getCause() instanceof JSONException); - } - } - @Test public void canConvertLongs() throws IOException { List longs = Arrays.asList(Long.MAX_VALUE, Long.MIN_VALUE); for (Long original : longs) { - String jsonString = JsonMapper.serializeJsonValue(original); + String jsonString = JsonMapper.serializeJson(original); long converted = (Long) JsonMapper.parseJsonValue(jsonString); assertEquals((long) original, converted); } @@ -86,15 +72,13 @@ public void canConvertLongs() throws IOException { public void canConvertDoubles() throws IOException { List doubles = Arrays.asList(Double.MAX_VALUE, Double.MIN_VALUE, Double.MIN_NORMAL); for (Double original : doubles) { - String jsonString = JsonMapper.serializeJsonValue(original); + String jsonString = JsonMapper.serializeJson(original); double converted = (Double) JsonMapper.parseJsonValue(jsonString); assertEquals(original, converted, 0); } } @Test - @Ignore - // TODO: Stop ignoring this test once JSON parsing has been fixed. public void canNest33LevelsDeep() throws IOException { Map root = new HashMap<>(); Map currentMap = root; @@ -103,7 +87,7 @@ public void canNest33LevelsDeep() throws IOException { currentMap.put("key", newMap); currentMap = newMap; } - String jsonString = JsonMapper.serializeJsonValue(root); + String jsonString = JsonMapper.serializeJson(root); Object value = JsonMapper.parseJsonValue(jsonString); assertEquals(root, value); } From 1f574a3e55c1fb405e8898ba4d5b6b118281719b Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 1 Feb 2019 11:47:20 -0800 Subject: [PATCH 050/456] Fixing some compiler warnings (#245) --- .../firebase/database/OnDisconnectTest.java | 16 +-- .../core/utilities/ChildKeyGenerator.java | 98 ------------------- .../FirebaseDatabaseAuthTestIT.java | 3 +- .../database/integration/OrderByTestIT.java | 3 +- .../testing/IntegrationTestUtils.java | 46 +++++---- 5 files changed, 39 insertions(+), 127 deletions(-) delete mode 100644 src/test/java/com/google/firebase/database/core/utilities/ChildKeyGenerator.java diff --git a/src/test/java/com/google/firebase/database/OnDisconnectTest.java b/src/test/java/com/google/firebase/database/OnDisconnectTest.java index 722840e9b..7d3174883 100644 --- a/src/test/java/com/google/firebase/database/OnDisconnectTest.java +++ b/src/test/java/com/google/firebase/database/OnDisconnectTest.java @@ -24,9 +24,11 @@ import com.google.firebase.database.DatabaseReference.CompletionListener; import com.google.firebase.database.core.Path; import com.google.firebase.database.core.Repo; +import com.google.firebase.database.snapshot.Node; import com.google.firebase.database.snapshot.NodeUtilities; import java.util.Map; import org.junit.Test; +import org.mockito.ArgumentMatchers; import org.mockito.Mockito; public class OnDisconnectTest { @@ -34,11 +36,11 @@ public class OnDisconnectTest { private static final Path path = new Path("foo"); @Test - public void testSetValue() throws Exception { + public void testSetValue() { Repo repo = mockRepo(); OnDisconnect reference = new OnDisconnect(repo, path); reference.setValueAsync("value"); - reference.setValue("value", (CompletionListener) null); + reference.setValue("value", null); Mockito.verify(repo, times(2)) .scheduleNow(Mockito.any(Runnable.class)); Mockito.verify(repo, times(2)) @@ -49,7 +51,7 @@ public void testSetValue() throws Exception { } @Test - public void testSetValueWithPriority() throws Exception { + public void testSetValueWithPriority() { Repo repo = mockRepo(); OnDisconnect reference = new OnDisconnect(repo, path); reference.setValueAsync("value", 10); @@ -89,7 +91,7 @@ public void testSetValueWithPriority() throws Exception { } @Test - public void testUpdateChildren() throws Exception { + public void testUpdateChildren() { Repo repo = mockRepo(); OnDisconnect reference = new OnDisconnect(repo, path); try { @@ -107,13 +109,13 @@ public void testUpdateChildren() throws Exception { Mockito.verify(repo, times(2)) .onDisconnectUpdate( Mockito.same(path), - Mockito.any(Map.class), + ArgumentMatchers.>any(), Mockito.any(CompletionListener.class), Mockito.same(update)); } @Test - public void testRemoveValue() throws Exception { + public void testRemoveValue() { Repo repo = mockRepo(); OnDisconnect reference = new OnDisconnect(repo, path); reference.removeValueAsync(); @@ -129,7 +131,7 @@ public void testRemoveValue() throws Exception { } @Test - public void testCancel() throws Exception { + public void testCancel() { Repo repo = mockRepo(); OnDisconnect reference = new OnDisconnect(repo, path); reference.cancelAsync(); diff --git a/src/test/java/com/google/firebase/database/core/utilities/ChildKeyGenerator.java b/src/test/java/com/google/firebase/database/core/utilities/ChildKeyGenerator.java deleted file mode 100644 index f3560923b..000000000 --- a/src/test/java/com/google/firebase/database/core/utilities/ChildKeyGenerator.java +++ /dev/null @@ -1,98 +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.core.utilities; - -import com.google.firebase.database.snapshot.ChildKey; -import net.java.quickcheck.Generator; -import net.java.quickcheck.generator.support.CharacterGenerator; -import net.java.quickcheck.generator.support.DoubleGenerator; -import net.java.quickcheck.generator.support.IntegerGenerator; - -public class ChildKeyGenerator implements Generator { - - private static final double PRIORITY_PROBABILITY = 0.05; - private static final double INTEGER_KEY_PROBABILITY = 0.10; - - private final IntegerGenerator length; - private final CharacterGenerator latinCharacters; - private final CharacterGenerator unicodeCharacters; - private final IntegerGenerator characterChooser; - private final IntegerGenerator integerKeyGenerator; - private final DoubleGenerator priority; - private final boolean includePriority; - - // CSOFF: AvoidEscapedUnicodeCharactersCheck - public ChildKeyGenerator(int maxSize, boolean includePriority) { - this.latinCharacters = new CharacterGenerator('\u0020', '\u007e'); - this.unicodeCharacters = new CharacterGenerator('\u0000', '\uffff'); - this.characterChooser = new IntegerGenerator(0, 10); - this.integerKeyGenerator = new IntegerGenerator(); - this.length = new IntegerGenerator(1, maxSize); - this.priority = new DoubleGenerator(0, 1); - this.includePriority = includePriority; - } - - private boolean isValidChildKeyCharacter(char c) { - // disallow ASCII control characters - if (c <= '\u001F') { - return false; - } - // reserved code points, can't be encoded in utf-8 - if (c >= '\ud800' && c <= '\udfff') { - return false; - } - switch (c) { - case '[': - case ']': - case '.': - case '#': - case '$': - case '/': - case '\u007F': - // disallow any of [].#$/\u007f - return false; - default: - // allow everything else - return true; - } - } - //CSON: AvoidEscapedUnicodeCharactersCheck - - @Override - public ChildKey next() { - if (this.includePriority && this.priority.nextDouble() < PRIORITY_PROBABILITY) { - return ChildKey.getPriorityKey(); - } else if (this.priority.nextDouble() < INTEGER_KEY_PROBABILITY) { - return ChildKey.fromString(String.valueOf(this.integerKeyGenerator.next())); - } else { - int size = this.length.nextInt(); - StringBuilder builder = new StringBuilder(); - - for (int count = 0; count < size; ++count) { - char next; - do { - boolean unicode = this.characterChooser.nextInt() == 0; - next = unicode ? this.unicodeCharacters.nextChar() : this.latinCharacters.nextChar(); - } - while (!isValidChildKeyCharacter(next)); - builder.append(next); - } - - return ChildKey.fromString(builder.toString()); - } - } -} diff --git a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java index 2d7a58141..d210039cc 100644 --- a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java @@ -23,6 +23,7 @@ import com.google.api.core.ApiFutures; import com.google.auth.oauth2.GoogleCredentials; import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.database.DataSnapshot; @@ -169,7 +170,7 @@ public void onSuccess(Void result) { success.compareAndSet(false, true); lock.countDown(); } - }); + }, MoreExecutors.directExecutor()); boolean finished = lock.await(TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); if (shouldTimeout) { assertTrue("Write finished (expected to timeout).", !finished); diff --git a/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java b/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java index 128194b1d..f1cc99dca 100644 --- a/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import com.google.api.client.json.GenericJson; import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.database.ChildEventListener; @@ -82,7 +83,7 @@ private static String formatRules(DatabaseReference ref, String rules) { return String.format( "{\"rules\": {\".read\": true, \".write\": true, \"%s\": %s}}", ref.getKey(), rules); } - + private static void uploadRules(FirebaseApp app, String rules) throws IOException { IntegrationTestUtils.AppHttpClient client = new IntegrationTestUtils.AppHttpClient(app); IntegrationTestUtils.ResponseInfo response = client.put("/.settings/rules.json", rules); diff --git a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java index bbfc24ea6..d3abaa096 100644 --- a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java +++ b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java @@ -19,13 +19,18 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.ByteArrayContent; +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.json.GenericJson; import com.google.cloud.firestore.FirestoreOptions; import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; 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; @@ -35,13 +40,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.List; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.util.EntityUtils; public class IntegrationTestUtils { @@ -151,8 +149,10 @@ public static List getRandomNode(FirebaseApp app, int count) } public static class AppHttpClient { - + private final FirebaseApp app; + private final FirebaseOptions options; + private final HttpRequestFactory requestFactory; public AppHttpClient() { this(FirebaseApp.getInstance()); @@ -160,17 +160,23 @@ public AppHttpClient() { public AppHttpClient(FirebaseApp app) { this.app = checkNotNull(app); + this.options = app.getOptions(); + this.requestFactory = this.options.getHttpTransport().createRequestFactory(); } - public ResponseInfo put(String path, String data) throws IOException { - String url = app.getOptions().getDatabaseUrl() + path + "?access_token=" + getToken(); - HttpPut put = new HttpPut(url); - HttpEntity entity = new StringEntity(data, "UTF-8"); - put.setEntity(entity); - - HttpClient httpClient = new DefaultHttpClient(); - HttpResponse response = httpClient.execute(put); - return new ResponseInfo(response); + public ResponseInfo put(String path, String json) throws IOException { + String url = options.getDatabaseUrl() + path + "?access_token=" + getToken(); + HttpRequest request = requestFactory.buildPutRequest(new GenericUrl(url), + ByteArrayContent.fromString("application/json", json)); + HttpResponse response = null; + try { + response = request.execute(); + return new ResponseInfo(response); + } finally { + if (response != null) { + response.disconnect(); + } + } } private String getToken() { @@ -185,8 +191,8 @@ public static class ResponseInfo { private final byte[] payload; private ResponseInfo(HttpResponse response) throws IOException { - this.status = response.getStatusLine().getStatusCode(); - this.payload = EntityUtils.toByteArray(response.getEntity()); + this.status = response.getStatusCode(); + this.payload = ByteStreams.toByteArray(response.getContent()); } public int getStatus() { From 36bbdce4f07a5cf6da42a4097d6963a99e7e1bd1 Mon Sep 17 00:00:00 2001 From: Arvind Mohabir Date: Mon, 4 Feb 2019 22:33:40 +0100 Subject: [PATCH 051/456] Fix for the uploadAccountResponse for correct error message/reason (#244) * - Fix for the uploadAccountResponse for correct error message/reason * Update CHANGELOG.md --- CHANGELOG.md | 1 + .../google/firebase/auth/internal/UploadAccountResponse.java | 2 +- src/test/resources/importUsersError.json | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7763b857f..5e9221972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - [changed] Removed org.json dependency and replaced with com.google.code.gson. - [changed] Upgraded Mockito dependency, and fixed the build on Java 11. +- [fixed] UploadAccountResponse now has the correct error message/reason. # v6.7.0 diff --git a/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java b/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java index abf28539d..544617cbd 100644 --- a/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java +++ b/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java @@ -35,7 +35,7 @@ public static class ErrorInfo { @Key("index") private int index; - @Key("message") + @Key("errorMessage") private String message; public int getIndex() { diff --git a/src/test/resources/importUsersError.json b/src/test/resources/importUsersError.json index 9233f8a34..6cffde2df 100644 --- a/src/test/resources/importUsersError.json +++ b/src/test/resources/importUsersError.json @@ -1,6 +1,6 @@ { "error": [ - {"index": 0, "message": "Some error occurred in user1"}, - {"index": 2, "message": "Another error occurred in user3"} + {"index": 0, "errorMessage": "Some error occurred in user1"}, + {"index": 2, "errorMessage": "Another error occurred in user3"} ] } From 13eaaa00332e69f3c33a043b7e8afa9204fa4696 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 19 Feb 2019 14:38:14 -0800 Subject: [PATCH 052/456] Revert "Fix for the uploadAccountResponse for correct error message/reason (#244)" (#251) This reverts commit 36bbdce4f07a5cf6da42a4097d6963a99e7e1bd1. --- CHANGELOG.md | 1 - .../google/firebase/auth/internal/UploadAccountResponse.java | 2 +- src/test/resources/importUsersError.json | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e9221972..7763b857f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ - [changed] Removed org.json dependency and replaced with com.google.code.gson. - [changed] Upgraded Mockito dependency, and fixed the build on Java 11. -- [fixed] UploadAccountResponse now has the correct error message/reason. # v6.7.0 diff --git a/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java b/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java index 544617cbd..abf28539d 100644 --- a/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java +++ b/src/main/java/com/google/firebase/auth/internal/UploadAccountResponse.java @@ -35,7 +35,7 @@ public static class ErrorInfo { @Key("index") private int index; - @Key("errorMessage") + @Key("message") private String message; public int getIndex() { diff --git a/src/test/resources/importUsersError.json b/src/test/resources/importUsersError.json index 6cffde2df..9233f8a34 100644 --- a/src/test/resources/importUsersError.json +++ b/src/test/resources/importUsersError.json @@ -1,6 +1,6 @@ { "error": [ - {"index": 0, "errorMessage": "Some error occurred in user1"}, - {"index": 2, "errorMessage": "Another error occurred in user3"} + {"index": 0, "message": "Some error occurred in user1"}, + {"index": 2, "message": "Another error occurred in user3"} ] } From 5ddf8fc5e5995af4758604f63197969cfad1ff76 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 4 Mar 2019 14:05:24 -0800 Subject: [PATCH 053/456] Speeding up the Project Management Unit Tests (#256) * Speeding up project mgt unit tests * Moved FirebaseAppScheduler out of the interface file * Removed extra whitespace --- .../FirebaseProjectManagementServiceImpl.java | 40 ++++++++++++++----- .../firebase/projectmanagement/Scheduler.java | 25 ++++++++++++ ...ebaseProjectManagementServiceImplTest.java | 14 +++++-- 3 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/google/firebase/projectmanagement/Scheduler.java diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java index 6ce7f3514..72c570c6d 100644 --- a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java +++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java @@ -16,10 +16,12 @@ package com.google.firebase.projectmanagement; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; 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.client.util.Sleeper; import com.google.api.core.ApiAsyncFunction; import com.google.api.core.ApiFunction; import com.google.api.core.ApiFuture; @@ -51,6 +53,8 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS private static final String IOS_NAMESPACE_PROPERTY = "bundle_id"; private final FirebaseApp app; + private final Sleeper sleeper; + private final Scheduler scheduler; private final HttpHelper httpHelper; private final CreateAndroidAppFromAppIdFunction createAndroidAppFromAppIdFunction = @@ -59,7 +63,13 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS new CreateIosAppFromAppIdFunction(); FirebaseProjectManagementServiceImpl(FirebaseApp app) { - this.app = app; + this(app, Sleeper.DEFAULT, new FirebaseAppScheduler(app)); + } + + FirebaseProjectManagementServiceImpl(FirebaseApp app, Sleeper sleeper, Scheduler scheduler) { + this.app = checkNotNull(app); + this.sleeper = checkNotNull(sleeper); + this.scheduler = checkNotNull(scheduler); this.httpHelper = new HttpHelper( app.getOptions().getJsonFactory(), app.getOptions().getHttpTransport().createRequestFactory( @@ -187,7 +197,7 @@ protected List execute() throws FirebaseProjectManagementException { projectId, platformResourceName, MAXIMUM_LIST_APPS_PAGE_SIZE); - ImmutableList.Builder builder = ImmutableList.builder(); + ImmutableList.Builder builder = ImmutableList.builder(); ListAppsResponse parsedResponse; do { parsedResponse = new ListAppsResponse(); @@ -361,10 +371,9 @@ private WaitOperationFunction(String projectId) { * or an exception if an error occurred during polling. */ @Override - public ApiFuture apply(String operationName) throws FirebaseProjectManagementException { - SettableApiFuture settableFuture = SettableApiFuture.create(); - ImplFirebaseTrampolines.schedule( - app, + public ApiFuture apply(String operationName) { + SettableApiFuture settableFuture = SettableApiFuture.create(); + scheduler.schedule( new WaitOperationRunnable( /* numberOfPreviousPolls= */ 0, operationName, @@ -417,8 +426,7 @@ public void run() { long delayMillis = (long) ( POLL_BASE_WAIT_TIME_MILLIS * Math.pow(POLL_EXPONENTIAL_BACKOFF_FACTOR, numberOfPreviousPolls + 1)); - ImplFirebaseTrampolines.schedule( - app, + scheduler.schedule( new WaitOperationRunnable( numberOfPreviousPolls + 1, operationName, @@ -725,12 +733,26 @@ public static class ShaCertificateResponse { private String certType; } + private static class FirebaseAppScheduler implements Scheduler { + + private final FirebaseApp app; + + FirebaseAppScheduler(FirebaseApp app) { + this.app = checkNotNull(app); + } + + @Override + public void schedule(Runnable runnable, long delayMillis) { + ImplFirebaseTrampolines.schedule(app, runnable, delayMillis); + } + } + /* Helper methods. */ private void sleepOrThrow(String projectId, long delayMillis) throws FirebaseProjectManagementException { try { - Thread.sleep(delayMillis); + sleeper.sleep(delayMillis); } catch (InterruptedException e) { throw HttpHelper.createFirebaseProjectManagementException( projectId, diff --git a/src/main/java/com/google/firebase/projectmanagement/Scheduler.java b/src/main/java/com/google/firebase/projectmanagement/Scheduler.java new file mode 100644 index 000000000..3df12a94a --- /dev/null +++ b/src/main/java/com/google/firebase/projectmanagement/Scheduler.java @@ -0,0 +1,25 @@ +/* + * Copyright 2019 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; + +/** + * Schedules a task to be executed after a specified delay. + */ +interface Scheduler { + + void schedule(Runnable runnable, long delayMillis); +} diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java index edf620ca5..a2fe8728d 100644 --- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java @@ -34,6 +34,7 @@ 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.testing.util.MockSleeper; import com.google.api.client.util.Base64; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; @@ -341,7 +342,7 @@ public void listIosAppsMultiplePages() throws Exception { MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); secondRpcResponse.setContent(LIST_IOS_APPS_PAGE_2_RESPONSE); serviceImpl = initServiceImpl( - ImmutableList.of(firstRpcResponse, secondRpcResponse), + ImmutableList.of(firstRpcResponse, secondRpcResponse), interceptor); List iosAppList = serviceImpl.listIosApps(PROJECT_ID); @@ -931,7 +932,7 @@ private static FirebaseProjectManagementServiceImpl initServiceImpl( .build(); FirebaseApp app = FirebaseApp.initializeApp(options); FirebaseProjectManagementServiceImpl serviceImpl = - new FirebaseProjectManagementServiceImpl(app); + new FirebaseProjectManagementServiceImpl(app, new MockSleeper(), new MockScheduler()); serviceImpl.setInterceptor(interceptor); return serviceImpl; } @@ -985,7 +986,7 @@ private enum HttpMethod { /** * Can be used to intercept multiple HTTP requests and responses made by the SDK during tests. */ - private class MultiRequestTestResponseInterceptor implements HttpResponseInterceptor { + private static class MultiRequestTestResponseInterceptor implements HttpResponseInterceptor { private final List responsesList = new ArrayList<>(); @Override @@ -1001,4 +1002,11 @@ public HttpResponse getResponse(int index) { return responsesList.get(index); } } + + private static class MockScheduler implements Scheduler { + @Override + public void schedule(Runnable runnable, long delayMillis) { + runnable.run(); + } + } } From e19ca297ad8314fef180e3822840bba80ab9f69c Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 13 Mar 2019 17:00:35 -0700 Subject: [PATCH 054/456] Implementing FCM sendAll and sendMulticast APIs (#247) * Preliminary impl of FCM batch support * Added BatchMessage class and tests * Refactored the FCM send operation * Added unit tests for sendBatch() API * Refactored error handling * Refactored FCM logic into a new FirebaseMessagingClient class * Using a separate request factory for child requests * Added the InstanceIdClient class for handling topic mgt ops * Added license headers * Renamed BatchResponse as SendResponse * Added BatchResponse class; Renamed BatchMessage to MulticastMessage * Updated tests and docs * Updated documentation * Added documentation and tests * Updated docs, changelog and annotations * Updated integration test to use multiple topics; Updated docs * Removing a redundant whitespace * Reduced FCM batch size to 100 (#250) * Setting the X-Client-Version header for FCM (#252) * Snippets for FCM sendAll() and sendMulticast() (#249) * Snippets for FCM sendAll() and sendMulticast() * Reduced batch size to 100 in snippets * Addressing some documentation nits --- .gitignore | 2 + CHANGELOG.md | 2 + .../firebase/internal/ApiClientUtils.java | 50 ++ .../firebase/messaging/BatchResponse.java | 56 ++ .../firebase/messaging/FirebaseMessaging.java | 454 +++++------ .../messaging/FirebaseMessagingClient.java | 243 ++++++ .../firebase/messaging/InstanceIdClient.java | 166 ++++ .../google/firebase/messaging/Message.java | 51 +- .../firebase/messaging/MulticastMessage.java | 206 +++++ .../firebase/messaging/SendResponse.java | 79 ++ .../messaging/TopicManagementResponse.java | 8 +- .../MessagingServiceErrorResponse.java | 46 ++ .../internal/MessagingServiceResponse.java | 16 + .../messaging/internal/package-info.java | 20 + .../firebase/messaging/BatchResponseTest.java | 79 ++ .../messaging/FirebaseMessagingIT.java | 92 ++- .../messaging/FirebaseMessagingTest.java | 730 ++++++++++++++++-- .../firebase/messaging/MessageTest.java | 23 +- .../messaging/MulticastMessageTest.java | 102 +++ .../firebase/messaging/SendResponseTest.java | 63 ++ .../snippets/FirebaseMessagingSnippets.java | 80 ++ .../testing/TestResponseInterceptor.java | 10 +- src/test/resources/fcm_batch_failure.txt | 80 ++ src/test/resources/fcm_batch_success.txt | 29 + 24 files changed, 2350 insertions(+), 337 deletions(-) create mode 100644 src/main/java/com/google/firebase/internal/ApiClientUtils.java create mode 100644 src/main/java/com/google/firebase/messaging/BatchResponse.java create mode 100644 src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java create mode 100644 src/main/java/com/google/firebase/messaging/InstanceIdClient.java create mode 100644 src/main/java/com/google/firebase/messaging/MulticastMessage.java create mode 100644 src/main/java/com/google/firebase/messaging/SendResponse.java create mode 100644 src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java create mode 100644 src/main/java/com/google/firebase/messaging/internal/MessagingServiceResponse.java create mode 100644 src/main/java/com/google/firebase/messaging/internal/package-info.java create mode 100644 src/test/java/com/google/firebase/messaging/BatchResponseTest.java create mode 100644 src/test/java/com/google/firebase/messaging/MulticastMessageTest.java create mode 100644 src/test/java/com/google/firebase/messaging/SendResponseTest.java create mode 100644 src/test/resources/fcm_batch_failure.txt create mode 100644 src/test/resources/fcm_batch_success.txt diff --git a/.gitignore b/.gitignore index 49d03c7a3..a869aa65d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ target/ .classpath .project .checkstyle +.factorypath +.vscode/ release.properties integration_cert.json integration_apikey.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 7763b857f..e669a2855 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- [added] Implemented new `sendAll()` and `sendMulticast()` APIs in + `FirebaseMessaging`. - [changed] Removed org.json dependency and replaced with com.google.code.gson. - [changed] Upgraded Mockito dependency, and fixed the build on Java 11. diff --git a/src/main/java/com/google/firebase/internal/ApiClientUtils.java b/src/main/java/com/google/firebase/internal/ApiClientUtils.java new file mode 100644 index 000000000..62e4320e4 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ApiClientUtils.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.firebase.FirebaseApp; + +import java.io.IOException; + +/** + * A set of shared utilities for using the Google API client. + */ +public class ApiClientUtils { + + public static HttpRequestFactory newAuthorizedRequestFactory(FirebaseApp app) { + HttpTransport transport = app.getOptions().getHttpTransport(); + return transport.createRequestFactory(new FirebaseRequestInitializer(app)); + } + + public static HttpRequestFactory newUnauthorizedRequestFactory(FirebaseApp app) { + HttpTransport transport = app.getOptions().getHttpTransport(); + return transport.createRequestFactory(); + } + + public static void disconnectQuietly(HttpResponse response) { + if (response != null) { + try { + response.disconnect(); + } catch (IOException ignored) { + // ignored + } + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/BatchResponse.java b/src/main/java/com/google/firebase/messaging/BatchResponse.java new file mode 100644 index 000000000..bd5069f4c --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/BatchResponse.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 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 com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; +import java.util.List; + +/** + * Response from an operation that sends FCM messages to multiple recipients. + * See {@link FirebaseMessaging#sendAll(List)} and {@link + * FirebaseMessaging#sendMulticast(MulticastMessage)}. + */ +public final class BatchResponse { + + private final List responses; + private final int successCount; + + BatchResponse(List responses) { + this.responses = ImmutableList.copyOf(responses); + int successCount = 0; + for (SendResponse response : this.responses) { + if (response.isSuccessful()) { + successCount++; + } + } + this.successCount = successCount; + } + + @NonNull + public List getResponses() { + return responses; + } + + public int getSuccessCount() { + return successCount; + } + + public int getFailureCount() { + return responses.size() - successCount; + } +} diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 34ed36497..49b85b27b 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -18,33 +18,20 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -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.HttpTransport; -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.json.JsonParser; -import com.google.api.client.util.Key; import com.google.api.core.ApiFuture; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.internal.CallableOperation; -import com.google.firebase.internal.FirebaseRequestInitializer; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; -import java.io.IOException; +import com.google.firebase.internal.Nullable; + import java.util.List; -import java.util.Map; /** * This class is the entry point for all server-side Firebase Cloud Messaging actions. @@ -54,60 +41,23 @@ */ 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.FcmError"; - - private static final String INTERNAL_ERROR = "internal-error"; - private static final String UNKNOWN_ERROR = "unknown-error"; - private static final Map FCM_ERROR_CODES = - ImmutableMap.builder() - // FCM v1 canonical error codes - .put("NOT_FOUND", "registration-token-not-registered") - .put("PERMISSION_DENIED", "mismatched-credential") - .put("RESOURCE_EXHAUSTED", "message-rate-exceeded") - .put("UNAUTHENTICATED", "invalid-apns-credentials") - - // FCM v1 new error codes - .put("APNS_AUTH_ERROR", "invalid-apns-credentials") - .put("INTERNAL", INTERNAL_ERROR) - .put("INVALID_ARGUMENT", "invalid-argument") - .put("QUOTA_EXCEEDED", "message-rate-exceeded") - .put("SENDER_ID_MISMATCH", "mismatched-credential") - .put("UNAVAILABLE", "server-unavailable") - .put("UNREGISTERED", "registration-token-not-registered") - .build(); - static final Map IID_ERROR_CODES = - ImmutableMap.builder() - .put(400, "invalid-argument") - .put(401, "authentication-error") - .put(403, "authentication-error") - .put(500, INTERNAL_ERROR) - .put(503, "server-unavailable") - .build(); - - private static final String IID_HOST = "https://iid.googleapis.com"; - private static final String IID_SUBSCRIBE_PATH = "iid/v1:batchAdd"; - private static final String IID_UNSUBSCRIBE_PATH = "iid/v1:batchRemove"; + static final String INTERNAL_ERROR = "internal-error"; - private final FirebaseApp app; - private final HttpRequestFactory requestFactory; - private final JsonFactory jsonFactory; - private final String url; + static final String UNKNOWN_ERROR = "unknown-error"; - private HttpResponseInterceptor interceptor; + private final FirebaseApp app; + private final FirebaseMessagingClient messagingClient; + private final InstanceIdClient instanceIdClient; private FirebaseMessaging(FirebaseApp app) { - HttpTransport httpTransport = app.getOptions().getHttpTransport(); - this.app = app; - this.requestFactory = httpTransport.createRequestFactory(new FirebaseRequestInitializer(app)); - this.jsonFactory = app.getOptions().getJsonFactory(); - String projectId = ImplFirebaseTrampolines.getProjectId(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 GOOGLE_CLOUD_PROJECT environment variable."); - this.url = String.format(FCM_URL, projectId); + this(app, null); + } + + @VisibleForTesting + FirebaseMessaging(FirebaseApp app, @Nullable HttpResponseInterceptor responseInterceptor) { + this.app = checkNotNull(app, "app must not be null"); + this.messagingClient = new FirebaseMessagingClient(app, responseInterceptor); + this.instanceIdClient = new InstanceIdClient(app, responseInterceptor); } /** @@ -138,6 +88,8 @@ public static synchronized FirebaseMessaging getInstance(FirebaseApp app) { * * @param message A non-null {@link Message} to be sent. * @return A message ID string. + * @throws FirebaseMessagingException If an error occurs while handing the message off to FCM for + * delivery. */ public String send(@NonNull Message message) throws FirebaseMessagingException { return send(message, false); @@ -152,6 +104,8 @@ public String send(@NonNull Message message) throws FirebaseMessagingException { * @param message A non-null {@link Message} to be sent. * @param dryRun a boolean indicating whether to perform a dry run (validation only) of the send. * @return A message ID string. + * @throws FirebaseMessagingException If an error occurs while handing the message off to FCM for + * delivery. */ public String send(@NonNull Message message, boolean dryRun) throws FirebaseMessagingException { return sendOp(message, dryRun).call(); @@ -180,6 +134,170 @@ public ApiFuture sendAsync(@NonNull Message message, boolean dryRun) { return sendOp(message, dryRun).callAsync(app); } + private CallableOperation sendOp( + final Message message, final boolean dryRun) { + checkNotNull(message, "message must not be null"); + return new CallableOperation() { + @Override + protected String execute() throws FirebaseMessagingException { + return messagingClient.send(message, dryRun); + } + }; + } + + /** + * Sends all the messages in the given list via Firebase Cloud Messaging. Employs batching to + * send the entire list as a single RPC call. Compared to the {@link #send(Message)} method, this + * is a significantly more efficient way to send multiple messages. + * + *

      The responses list obtained by calling {@link BatchResponse#getResponses()} on the return + * value corresponds to the order of input messages. + * + * @param messages A non-null, non-empty list containing up to 100 messages. + * @return A {@link BatchResponse} indicating the result of the operation. + * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for + * delivery. An exception here indicates a total failure -- i.e. none of the messages in the + * list could be sent. Partial failures are indicated by a {@link BatchResponse} return value. + */ + public BatchResponse sendAll( + @NonNull List messages) throws FirebaseMessagingException { + return sendAll(messages, false); + } + + /** + * Sends all the messages in the given list via Firebase Cloud Messaging. Employs batching to + * send the entire list as a single RPC call. Compared to the {@link #send(Message)} method, this + * is a significantly more efficient way to send multiple messages. + * + *

      If the {@code dryRun} option is set to true, the messages will not be actually sent. Instead + * FCM performs all the necessary validations, and emulates the send operation. + * + *

      The responses list obtained by calling {@link BatchResponse#getResponses()} on the return + * value corresponds to the order of input messages. + * + * @param messages A non-null, non-empty list containing up to 100 messages. + * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. + * @return A {@link BatchResponse} indicating the result of the operation. + * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for + * delivery. An exception here indicates a total failure -- i.e. none of the messages in the + * list could be sent. Partial failures are indicated by a {@link BatchResponse} return value. + */ + public BatchResponse sendAll( + @NonNull List messages, boolean dryRun) throws FirebaseMessagingException { + return sendAllOp(messages, dryRun).call(); + } + + /** + * Similar to {@link #sendAll(List)} but performs the operation asynchronously. + * + * @param messages A non-null, non-empty list containing up to 100 messages. + * @return @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * the messages have been sent. + */ + public ApiFuture sendAllAsync(@NonNull List messages) { + return sendAllAsync(messages, false); + } + + /** + * Similar to {@link #sendAll(List, boolean)} but performs the operation asynchronously. + * + * @param messages A non-null, non-empty list containing up to 100 messages. + * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. + * @return @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * the messages have been sent, or when the emulation has finished. + */ + public ApiFuture sendAllAsync( + @NonNull List messages, boolean dryRun) { + return sendAllOp(messages, dryRun).callAsync(app); + } + + /** + * Sends the given multicast message to all the FCM registration tokens specified in it. + * + *

      This method uses the {@link #sendAll(List)} API under the hood to send the given + * message to all the target recipients. The responses list obtained by calling + * {@link BatchResponse#getResponses()} on the return value corresponds to the order of tokens + * in the {@link MulticastMessage}. + * + * @param message A non-null {@link MulticastMessage} + * @return A {@link BatchResponse} indicating the result of the operation. + * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for + * delivery. An exception here indicates a total failure -- i.e. the messages could not be + * delivered to any recipient. Partial failures are indicated by a {@link BatchResponse} + * return value. + */ + public BatchResponse sendMulticast( + @NonNull MulticastMessage message) throws FirebaseMessagingException { + return sendMulticast(message, false); + } + + /** + * Sends the given multicast message to all the FCM registration tokens specified in it. + * + *

      If the {@code dryRun} option is set to true, the message will not be actually sent. Instead + * FCM performs all the necessary validations, and emulates the send operation. + * + *

      This method uses the {@link #sendAll(List)} API under the hood to send the given + * message to all the target recipients. The responses list obtained by calling + * {@link BatchResponse#getResponses()} on the return value corresponds to the order of tokens + * in the {@link MulticastMessage}. + * + * @param message A non-null {@link MulticastMessage}. + * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. + * @return A {@link BatchResponse} indicating the result of the operation. + * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for + * delivery. An exception here indicates a total failure -- i.e. the messages could not be + * delivered to any recipient. Partial failures are indicated by a {@link BatchResponse} + * return value. + */ + public BatchResponse sendMulticast( + @NonNull MulticastMessage message, boolean dryRun) throws FirebaseMessagingException { + checkNotNull(message, "multicast message must not be null"); + return sendAll(message.getMessageList(), dryRun); + } + + /** + * Similar to {@link #sendMulticast(MulticastMessage)} but performs the operation + * asynchronously. + * + * @param message A non-null {@link MulticastMessage}. + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * the messages have been sent. + */ + public ApiFuture sendMulticastAsync(@NonNull MulticastMessage message) { + return sendMulticastAsync(message, false); + } + + /** + * Similar to {@link #sendMulticast(MulticastMessage, boolean)} but performs the operation + * asynchronously. + * + * @param message A non-null {@link MulticastMessage}. + * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. + * @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when + * the messages have been sent. + */ + public ApiFuture sendMulticastAsync( + @NonNull MulticastMessage message, boolean dryRun) { + checkNotNull(message, "multicast message must not be null"); + return sendAllAsync(message.getMessageList(), dryRun); + } + + private CallableOperation sendAllOp( + final List messages, final boolean dryRun) { + + final List immutableMessages = ImmutableList.copyOf(messages); + checkArgument(!immutableMessages.isEmpty(), "messages list must not be empty"); + checkArgument(immutableMessages.size() <= 100, + "messages list must not contain more than 100 elements"); + return new CallableOperation() { + @Override + protected BatchResponse execute() throws FirebaseMessagingException { + return messagingClient.sendAll(messages, dryRun); + } + }; + } + /** * Subscribes a list of registration tokens to a topic. * @@ -190,7 +308,7 @@ public ApiFuture sendAsync(@NonNull Message message, boolean dryRun) { */ public TopicManagementResponse subscribeToTopic(@NonNull List registrationTokens, @NonNull String topic) throws FirebaseMessagingException { - return manageTopicOp(registrationTokens, topic, IID_SUBSCRIBE_PATH).call(); + return subscribeOp(registrationTokens, topic).call(); } /** @@ -203,11 +321,23 @@ public TopicManagementResponse subscribeToTopic(@NonNull List registrati */ public ApiFuture subscribeToTopicAsync( @NonNull List registrationTokens, @NonNull String topic) { - return manageTopicOp(registrationTokens, topic, IID_SUBSCRIBE_PATH).callAsync(app); + return subscribeOp(registrationTokens, topic).callAsync(app); + } + + private CallableOperation subscribeOp( + final List registrationTokens, final String topic) { + checkRegistrationTokens(registrationTokens); + checkTopic(topic); + return new CallableOperation() { + @Override + protected TopicManagementResponse execute() throws FirebaseMessagingException { + return instanceIdClient.subscribeToTopic(topic, registrationTokens); + } + }; } /** - * Unubscribes a list of registration tokens from a topic. + * Unsubscribes a list of registration tokens from a topic. * * @param registrationTokens A non-null, non-empty list of device registration tokens, with at * most 1000 entries. @@ -216,7 +346,7 @@ public ApiFuture subscribeToTopicAsync( */ public TopicManagementResponse unsubscribeFromTopic(@NonNull List registrationTokens, @NonNull String topic) throws FirebaseMessagingException { - return manageTopicOp(registrationTokens, topic, IID_UNSUBSCRIBE_PATH).call(); + return unsubscribeOp(registrationTokens, topic).call(); } /** @@ -230,146 +360,21 @@ public TopicManagementResponse unsubscribeFromTopic(@NonNull List regist */ public ApiFuture unsubscribeFromTopicAsync( @NonNull List registrationTokens, @NonNull String topic) { - return manageTopicOp(registrationTokens, topic, IID_UNSUBSCRIBE_PATH) - .callAsync(app); - } - - private CallableOperation sendOp( - final Message message, final boolean dryRun) { - checkNotNull(message, "message must not be null"); - return new CallableOperation() { - @Override - protected String execute() throws FirebaseMessagingException { - ImmutableMap.Builder payload = ImmutableMap.builder() - .put("message", message); - if (dryRun) { - payload.put("validate_only", true); - } - HttpResponse response = null; - 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(); - MessagingServiceResponse parsed = new MessagingServiceResponse(); - jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); - return parsed.name; - } catch (HttpResponseException e) { - handleSendHttpError(e); - return null; - } catch (IOException e) { - throw new FirebaseMessagingException( - INTERNAL_ERROR, "Error while calling FCM backend service", e); - } finally { - disconnectQuietly(response); - } - } - }; + return unsubscribeOp(registrationTokens, topic).callAsync(app); } - private void handleSendHttpError(HttpResponseException e) throws FirebaseMessagingException { - MessagingServiceErrorResponse response = new MessagingServiceErrorResponse(); - if (e.getContent() != null) { - try { - JsonParser parser = jsonFactory.createJsonParser(e.getContent()); - parser.parseAndClose(response); - } catch (IOException ignored) { - // ignored - } - } - String code = FCM_ERROR_CODES.get(response.getErrorCode()); - if (code == null) { - code = UNKNOWN_ERROR; - } - String msg = response.getErrorMessage(); - if (Strings.isNullOrEmpty(msg)) { - msg = String.format("Unexpected HTTP response with status: %d; body: %s", - e.getStatusCode(), e.getContent()); - } - throw new FirebaseMessagingException(code, msg, e); - } - - private CallableOperation - manageTopicOp( - final List registrationTokens, final String topic, final String path) { + private CallableOperation unsubscribeOp( + final List registrationTokens, final String topic) { checkRegistrationTokens(registrationTokens); checkTopic(topic); return new CallableOperation() { @Override protected TopicManagementResponse execute() throws FirebaseMessagingException { - final String prefixedTopic; - if (topic.startsWith("/topics/")) { - prefixedTopic = topic; - } else { - prefixedTopic = "/topics/" + topic; - } - Map payload = ImmutableMap.of( - "to", prefixedTopic, - "registration_tokens", registrationTokens - ); - - final String url = String.format("%s/%s", IID_HOST, path); - HttpResponse response = null; - try { - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); - request.getHeaders().set("access_token_auth", "true"); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(interceptor); - response = request.execute(); - InstanceIdServiceResponse parsed = new InstanceIdServiceResponse(); - jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); - checkState(parsed.results != null && !parsed.results.isEmpty(), - "unexpected response from topic management service"); - return new TopicManagementResponse(parsed.results); - } catch (HttpResponseException e) { - handleTopicManagementHttpError(e); - return null; - } catch (IOException e) { - throw new FirebaseMessagingException( - INTERNAL_ERROR, "Error while calling IID backend service", e); - } finally { - disconnectQuietly(response); - } + return instanceIdClient.unsubscribeFromTopic(topic, registrationTokens); } }; } - private void handleTopicManagementHttpError( - HttpResponseException e) throws FirebaseMessagingException { - InstanceIdServiceErrorResponse response = new InstanceIdServiceErrorResponse(); - try { - JsonParser parser = jsonFactory.createJsonParser(e.getContent()); - parser.parseAndClose(response); - } catch (IOException ignored) { - // ignored - } - - // Infer error code from HTTP status - String code = IID_ERROR_CODES.get(e.getStatusCode()); - if (code == null) { - code = UNKNOWN_ERROR; - } - String msg = response.error; - if (Strings.isNullOrEmpty(msg)) { - msg = String.format("Unexpected HTTP response with status: %d; body: %s", - e.getStatusCode(), e.getContent()); - } - throw new FirebaseMessagingException(code, msg, e); - } - - private static void disconnectQuietly(HttpResponse response) { - if (response != null) { - try { - response.disconnect(); - } catch (IOException ignored) { - // ignored - } - } - } - private static void checkRegistrationTokens(List registrationTokens) { checkArgument(registrationTokens != null && !registrationTokens.isEmpty(), "registrationTokens list must not be null or empty"); @@ -386,11 +391,6 @@ private static void checkTopic(String topic) { checkArgument(topic.matches("^(/topics/)?(private/)?[a-zA-Z0-9-_.~%]+$"), "invalid topic name"); } - @VisibleForTesting - void setInterceptor(HttpResponseInterceptor interceptor) { - this.interceptor = interceptor; - } - private static final String SERVICE_ID = FirebaseMessaging.class.getName(); private static class FirebaseMessagingService extends FirebaseService { @@ -406,50 +406,4 @@ public void destroy() { // which will throw once the app is deleted. } } - - private static class MessagingServiceResponse { - @Key("name") - private String name; - } - - private static class MessagingServiceErrorResponse { - @Key("error") - private Map error; - - - String getErrorCode() { - if (error == null) { - return null; - } - Object details = error.get("details"); - if (details != null && details instanceof List) { - for (Object detail : (List) details) { - if (detail instanceof Map) { - Map detailMap = (Map) detail; - if (FCM_ERROR_TYPE.equals(detailMap.get("@type"))) { - return (String) detailMap.get("errorCode"); - } - } - } - } - return (String) error.get("status"); - } - - String getErrorMessage() { - if (error != null) { - return (String) error.get("message"); - } - return null; - } - } - - private static class InstanceIdServiceResponse { - @Key("results") - private List> results; - } - - private static class InstanceIdServiceErrorResponse { - @Key("error") - private String error; - } } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java new file mode 100644 index 000000000..5e694ca4d --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java @@ -0,0 +1,243 @@ +/* + * Copyright 2019 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.api.client.googleapis.batch.BatchCallback; +import com.google.api.client.googleapis.batch.BatchRequest; +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.HttpRequestFactory; +import com.google.api.client.http.HttpRequestInitializer; +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.api.client.json.JsonParser; +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.ApiClientUtils; +import com.google.firebase.internal.Nullable; +import com.google.firebase.internal.SdkUtils; +import com.google.firebase.messaging.internal.MessagingServiceErrorResponse; +import com.google.firebase.messaging.internal.MessagingServiceResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * A helper class for interacting with Firebase Cloud Messaging service. + */ +final class FirebaseMessagingClient { + + private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; + + private static final String FCM_BATCH_URL = "https://fcm.googleapis.com/batch"; + + private static final Map FCM_ERROR_CODES = + ImmutableMap.builder() + // FCM v1 canonical error codes + .put("NOT_FOUND", "registration-token-not-registered") + .put("PERMISSION_DENIED", "mismatched-credential") + .put("RESOURCE_EXHAUSTED", "message-rate-exceeded") + .put("UNAUTHENTICATED", "invalid-apns-credentials") + + // FCM v1 new error codes + .put("APNS_AUTH_ERROR", "invalid-apns-credentials") + .put("INTERNAL", FirebaseMessaging.INTERNAL_ERROR) + .put("INVALID_ARGUMENT", "invalid-argument") + .put("QUOTA_EXCEEDED", "message-rate-exceeded") + .put("SENDER_ID_MISMATCH", "mismatched-credential") + .put("UNAVAILABLE", "server-unavailable") + .put("UNREGISTERED", "registration-token-not-registered") + .build(); + + private final String fcmSendUrl; + private final HttpRequestFactory requestFactory; + private final HttpRequestFactory childRequestFactory; + private final JsonFactory jsonFactory; + private final HttpResponseInterceptor responseInterceptor; + private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); + + FirebaseMessagingClient(FirebaseApp app, @Nullable HttpResponseInterceptor responseInterceptor) { + String projectId = ImplFirebaseTrampolines.getProjectId(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 GOOGLE_CLOUD_PROJECT environment variable."); + this.fcmSendUrl = String.format(FCM_URL, projectId); + this.requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + this.childRequestFactory = ApiClientUtils.newUnauthorizedRequestFactory(app); + this.jsonFactory = app.getOptions().getJsonFactory(); + this.responseInterceptor = responseInterceptor; + } + + String send(Message message, boolean dryRun) throws FirebaseMessagingException { + try { + return sendSingleRequest(message, dryRun); + } catch (HttpResponseException e) { + throw createExceptionFromResponse(e); + } catch (IOException e) { + throw new FirebaseMessagingException( + FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); + } + } + + BatchResponse sendAll( + List messages, boolean dryRun) throws FirebaseMessagingException { + try { + return sendBatchRequest(messages, dryRun); + } catch (HttpResponseException e) { + throw createExceptionFromResponse(e); + } catch (IOException e) { + throw new FirebaseMessagingException( + FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); + } + } + + private String sendSingleRequest(Message message, boolean dryRun) throws IOException { + HttpRequest request = requestFactory.buildPostRequest( + new GenericUrl(fcmSendUrl), + new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); + setCommonFcmHeaders(request.getHeaders()); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(responseInterceptor); + HttpResponse response = request.execute(); + try { + MessagingServiceResponse parsed = new MessagingServiceResponse(); + jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); + return parsed.getMessageId(); + } finally { + ApiClientUtils.disconnectQuietly(response); + } + } + + private BatchResponse sendBatchRequest( + List messages, boolean dryRun) throws IOException { + + MessagingBatchCallback callback = new MessagingBatchCallback(); + BatchRequest batch = newBatchRequest(messages, dryRun, callback); + batch.execute(); + return new BatchResponse(callback.getResponses()); + } + + private BatchRequest newBatchRequest( + List messages, boolean dryRun, MessagingBatchCallback callback) throws IOException { + + BatchRequest batch = new BatchRequest( + requestFactory.getTransport(), getBatchRequestInitializer()); + batch.setBatchUrl(new GenericUrl(FCM_BATCH_URL)); + + final JsonObjectParser jsonParser = new JsonObjectParser(this.jsonFactory); + final GenericUrl sendUrl = new GenericUrl(fcmSendUrl); + for (Message message : messages) { + // Using a separate request factory without authorization is faster for large batches. + // A simple performance test showed a 400-500ms speed up for batches of 1000 messages. + HttpRequest request = childRequestFactory.buildPostRequest( + sendUrl, + new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); + request.setParser(jsonParser); + setCommonFcmHeaders(request.getHeaders()); + batch.queue( + request, MessagingServiceResponse.class, MessagingServiceErrorResponse.class, callback); + } + + return batch; + } + + private void setCommonFcmHeaders(HttpHeaders headers) { + headers.set("X-GOOG-API-FORMAT-VERSION", "2"); + headers.set("X-Client-Version", clientVersion); + } + + private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { + MessagingServiceErrorResponse response = new MessagingServiceErrorResponse(); + if (e.getContent() != null) { + try { + JsonParser parser = jsonFactory.createJsonParser(e.getContent()); + parser.parseAndClose(response); + } catch (IOException ignored) { + // ignored + } + } + + return newException(response, e); + } + + private HttpRequestInitializer getBatchRequestInitializer() { + return new HttpRequestInitializer(){ + @Override + public void initialize(HttpRequest request) throws IOException { + requestFactory.getInitializer().initialize(request); + request.setResponseInterceptor(responseInterceptor); + } + }; + } + + private static FirebaseMessagingException newException(MessagingServiceErrorResponse response) { + return newException(response, null); + } + + private static FirebaseMessagingException newException( + MessagingServiceErrorResponse response, @Nullable HttpResponseException e) { + String code = FCM_ERROR_CODES.get(response.getErrorCode()); + if (code == null) { + code = FirebaseMessaging.UNKNOWN_ERROR; + } + + String msg = response.getErrorMessage(); + if (Strings.isNullOrEmpty(msg)) { + if (e != null) { + msg = String.format("Unexpected HTTP response with status: %d; body: %s", + e.getStatusCode(), e.getContent()); + } else { + msg = String.format("Unexpected HTTP response: %s", response.toString()); + } + } + + return new FirebaseMessagingException(code, msg, e); + } + + private static class MessagingBatchCallback + implements BatchCallback { + + private final ImmutableList.Builder responses = ImmutableList.builder(); + + @Override + public void onSuccess( + MessagingServiceResponse response, HttpHeaders responseHeaders) { + responses.add(SendResponse.fromMessageId(response.getMessageId())); + } + + @Override + public void onFailure( + MessagingServiceErrorResponse error, HttpHeaders responseHeaders) { + responses.add(SendResponse.fromException(newException(error))); + } + + List getResponses() { + return this.responses.build(); + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/InstanceIdClient.java b/src/main/java/com/google/firebase/messaging/InstanceIdClient.java new file mode 100644 index 000000000..017029094 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/InstanceIdClient.java @@ -0,0 +1,166 @@ +/* + * Copyright 2019 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 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.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.json.JsonParser; +import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.FirebaseApp; +import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.Nullable; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * A helper class for interacting with the Firebase Instance ID service. Implements the FCM + * topic management functionality. + */ +final class InstanceIdClient { + + private static final String IID_HOST = "https://iid.googleapis.com"; + + private static final String IID_SUBSCRIBE_PATH = "iid/v1:batchAdd"; + + private static final String IID_UNSUBSCRIBE_PATH = "iid/v1:batchRemove"; + + static final Map IID_ERROR_CODES = + ImmutableMap.builder() + .put(400, "invalid-argument") + .put(401, "authentication-error") + .put(403, "authentication-error") + .put(500, FirebaseMessaging.INTERNAL_ERROR) + .put(503, "server-unavailable") + .build(); + + private final HttpRequestFactory requestFactory; + private final JsonFactory jsonFactory; + private final HttpResponseInterceptor responseInterceptor; + + InstanceIdClient(FirebaseApp app, @Nullable HttpResponseInterceptor responseInterceptor) { + this.requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + this.jsonFactory = app.getOptions().getJsonFactory(); + this.responseInterceptor = responseInterceptor; + } + + TopicManagementResponse subscribeToTopic( + String topic, List registrationTokens) throws FirebaseMessagingException { + try { + return sendInstanceIdRequest(topic, registrationTokens, IID_SUBSCRIBE_PATH); + } catch (HttpResponseException e) { + throw createExceptionFromResponse(e); + } catch (IOException e) { + throw new FirebaseMessagingException( + FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); + } + } + + TopicManagementResponse unsubscribeFromTopic( + String topic, List registrationTokens) throws FirebaseMessagingException { + try { + return sendInstanceIdRequest(topic, registrationTokens, IID_UNSUBSCRIBE_PATH); + } catch (HttpResponseException e) { + throw createExceptionFromResponse(e); + } catch (IOException e) { + throw new FirebaseMessagingException( + FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); + } + } + + private TopicManagementResponse sendInstanceIdRequest( + String topic, List registrationTokens, String path) throws IOException { + String url = String.format("%s/%s", IID_HOST, path); + Map payload = ImmutableMap.of( + "to", getPrefixedTopic(topic), + "registration_tokens", registrationTokens + ); + HttpResponse response = null; + try { + HttpRequest request = requestFactory.buildPostRequest( + new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); + request.getHeaders().set("access_token_auth", "true"); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(responseInterceptor); + response = request.execute(); + + JsonParser parser = jsonFactory.createJsonParser(response.getContent()); + InstanceIdServiceResponse parsedResponse = new InstanceIdServiceResponse(); + parser.parse(parsedResponse); + return new TopicManagementResponse(parsedResponse.results); + } finally { + ApiClientUtils.disconnectQuietly(response); + } + } + + private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { + InstanceIdServiceErrorResponse response = new InstanceIdServiceErrorResponse(); + if (e.getContent() != null) { + try { + JsonParser parser = jsonFactory.createJsonParser(e.getContent()); + parser.parseAndClose(response); + } catch (IOException ignored) { + // ignored + } + } + return newException(response, e); + } + + private String getPrefixedTopic(String topic) { + if (topic.startsWith("/topics/")) { + return topic; + } else { + return "/topics/" + topic; + } + } + + private static FirebaseMessagingException newException( + InstanceIdServiceErrorResponse response, HttpResponseException e) { + // Infer error code from HTTP status + String code = IID_ERROR_CODES.get(e.getStatusCode()); + if (code == null) { + code = FirebaseMessaging.UNKNOWN_ERROR; + } + String msg = response.error; + if (Strings.isNullOrEmpty(msg)) { + msg = String.format("Unexpected HTTP response with status: %d; body: %s", + e.getStatusCode(), e.getContent()); + } + return new FirebaseMessagingException(code, msg, e); + } + + private static class InstanceIdServiceResponse { + @Key("results") + private List results; + } + + private static class InstanceIdServiceErrorResponse { + @Key("error") + private String error; + } +} diff --git a/src/main/java/com/google/firebase/messaging/Message.java b/src/main/java/com/google/firebase/messaging/Message.java index 77cce7ae3..89a4a5ce3 100644 --- a/src/main/java/com/google/firebase/messaging/Message.java +++ b/src/main/java/com/google/firebase/messaging/Message.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkArgument; import com.google.api.client.util.Key; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.primitives.Booleans; @@ -79,6 +80,55 @@ private Message(Builder builder) { this.condition = builder.condition; } + @VisibleForTesting + Map getData() { + return data; + } + + @VisibleForTesting + Notification getNotification() { + return notification; + } + + @VisibleForTesting + AndroidConfig getAndroidConfig() { + return androidConfig; + } + + @VisibleForTesting + WebpushConfig getWebpushConfig() { + return webpushConfig; + } + + @VisibleForTesting + ApnsConfig getApnsConfig() { + return apnsConfig; + } + + @VisibleForTesting + String getToken() { + return token; + } + + @VisibleForTesting + String getTopic() { + return topic; + } + + @VisibleForTesting + String getCondition() { + return condition; + } + + Map wrapForTransport(boolean dryRun) { + ImmutableMap.Builder payload = ImmutableMap.builder() + .put("message", this); + if (dryRun) { + payload.put("validate_only", true); + } + return payload.build(); + } + private static String stripPrefix(String topic) { if (Strings.isNullOrEmpty(topic)) { return null; @@ -226,5 +276,4 @@ public Message build() { return new Message(this); } } - } diff --git a/src/main/java/com/google/firebase/messaging/MulticastMessage.java b/src/main/java/com/google/firebase/messaging/MulticastMessage.java new file mode 100644 index 000000000..32c9365f1 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/MulticastMessage.java @@ -0,0 +1,206 @@ +/* + * Copyright 2019 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.api.client.util.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.NonNull; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a message that can be sent to multiple devices via Firebase Cloud Messaging (FCM). + * Contains payload information as well as the list of device registration tokens to which the + * message should be sent. A single {@code MulticastMessage} may contain up to 100 registration + * tokens. + * + *

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

      This class and the associated Builder retain the order of tokens. Therefore the order of + * the responses list obtained by calling {@link BatchResponse#getResponses()} on the return value + * of {@link FirebaseMessaging#sendMulticast(MulticastMessage)} corresponds to the order in which + * tokens were added to the {@link MulticastMessage.Builder}. + */ +public class MulticastMessage { + + private final List tokens; + private final Map data; + private final Notification notification; + private final AndroidConfig androidConfig; + private final WebpushConfig webpushConfig; + private final ApnsConfig apnsConfig; + + private MulticastMessage(Builder builder) { + this.tokens = builder.tokens.build(); + checkArgument(!this.tokens.isEmpty(), "at least one token must be specified"); + checkArgument(this.tokens.size() <= 100, "no more than 100 tokens can be specified"); + for (String token : this.tokens) { + checkArgument(!Strings.isNullOrEmpty(token), "none of the tokens can be null or empty"); + } + this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data); + this.notification = builder.notification; + this.androidConfig = builder.androidConfig; + this.webpushConfig = builder.webpushConfig; + this.apnsConfig = builder.apnsConfig; + } + + List getMessageList() { + Message.Builder builder = Message.builder() + .setNotification(this.notification) + .setAndroidConfig(this.androidConfig) + .setApnsConfig(this.apnsConfig) + .setWebpushConfig(this.webpushConfig); + if (this.data != null) { + builder.putAllData(this.data); + } + ImmutableList.Builder messages = ImmutableList.builder(); + for (String token : this.tokens) { + messages.add(builder.setToken(token).build()); + } + return messages.build(); + } + + /** + * Creates a new {@link MulticastMessage.Builder}. + * + * @return A {@link MulticastMessage.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private final ImmutableList.Builder tokens = ImmutableList.builder(); + private final Map data = new HashMap<>(); + private Notification notification; + private AndroidConfig androidConfig; + private WebpushConfig webpushConfig; + private ApnsConfig apnsConfig; + + private Builder() {} + + /** + * Adds a token to which the message should be sent. Up to 100 tokens can be specified on + * a single instance of {@link MulticastMessage}. + * + * @param token A non-null, non-empty Firebase device registration token. + * @return This builder. + */ + public Builder addToken(@NonNull String token) { + this.tokens.add(token); + return this; + } + + /** + * Adds a list of tokens to which the message should be sent. Up to 100 tokens can be + * specified on a single instance of {@link MulticastMessage}. + * + * @param tokens List of Firebase device registration tokens. + * @return This builder. + */ + public Builder addAllTokens(@NonNull List tokens) { + this.tokens.addAll(tokens); + return this; + } + + /** + * Sets the notification information to be included in the message. + * + * @param notification A {@link Notification} instance. + * @return This builder. + */ + public Builder setNotification(Notification notification) { + this.notification = notification; + return this; + } + + /** + * Sets the Android-specific information to be included in the message. + * + * @param androidConfig An {@link AndroidConfig} instance. + * @return This builder. + */ + public Builder setAndroidConfig(AndroidConfig androidConfig) { + this.androidConfig = androidConfig; + return this; + } + + /** + * Sets the Webpush-specific information to be included in the message. + * + * @param webpushConfig A {@link WebpushConfig} instance. + * @return This builder. + */ + public Builder setWebpushConfig(WebpushConfig webpushConfig) { + this.webpushConfig = webpushConfig; + return this; + } + + /** + * Sets the information specific to APNS (Apple Push Notification Service). + * + * @param apnsConfig An {@link ApnsConfig} instance. + * @return This builder. + */ + public Builder setApnsConfig(ApnsConfig apnsConfig) { + this.apnsConfig = apnsConfig; + return this; + } + + /** + * Adds the given key-value pair to the message as a data field. Key or the value may not be + * null. + * + * @param key Name of the data field. Must not be null. + * @param value Value of the data field. Must not be null. + * @return This builder. + */ + public Builder putData(@NonNull String key, @NonNull String value) { + this.data.put(key, value); + return this; + } + + /** + * Adds all the key-value pairs in the given map to the message as data fields. None of the + * keys or values may be null. + * + * @param map A non-null map of data fields. Map must not contain null keys or values. + * @return This builder. + */ + public Builder putAllData(@NonNull Map map) { + this.data.putAll(map); + return this; + } + + /** + * Creates a new {@link MulticastMessage} instance from the parameters set on this builder. + * + * @return A new {@link MulticastMessage} instance. + * @throws IllegalArgumentException If any of the parameters set on the builder are invalid. + */ + public MulticastMessage build() { + return new MulticastMessage(this); + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/SendResponse.java b/src/main/java/com/google/firebase/messaging/SendResponse.java new file mode 100644 index 000000000..b179df2b2 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/SendResponse.java @@ -0,0 +1,79 @@ +/* + * Copyright 2019 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Strings; +import com.google.firebase.internal.Nullable; + +/** + * The result of an individual send operation that was executed as part of a batch. See + * {@link BatchResponse} for more details. + */ +public final class SendResponse { + + private final String messageId; + private final FirebaseMessagingException exception; + + private SendResponse(String messageId, FirebaseMessagingException exception) { + this.messageId = messageId; + this.exception = exception; + } + + /** + * Returns a message ID string if the send operation was successful. Otherwise returns null. + * + * @return A message ID string or null. + */ + @Nullable + public String getMessageId() { + return this.messageId; + } + + /** + * Returns an exception if the send operation failed. Otherwise returns null. + * + * @return A {@link FirebaseMessagingException} or null. + */ + @Nullable + public FirebaseMessagingException getException() { + return this.exception; + } + + /** + * Returns whether the send operation was successful or not. When this method returns true, + * {@link #getMessageId()} is guaranteed to return a non-null value. When this method returns + * false {@link #getException()} is guaranteed to return a non-null value. + * + * @return A boolean indicating success of the operation. + */ + public boolean isSuccessful() { + return !Strings.isNullOrEmpty(this.messageId); + } + + static SendResponse fromMessageId(String messageId) { + checkArgument(!Strings.isNullOrEmpty(messageId), "messageId must not be null or empty"); + return new SendResponse(messageId, null); + } + + static SendResponse fromException(FirebaseMessagingException exception) { + checkNotNull(exception, "exception must not be null"); + return new SendResponse(null, exception); + } +} diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java index 230c64df2..b8f92576e 100644 --- a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java +++ b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java @@ -16,6 +16,10 @@ package com.google.firebase.messaging; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +import com.google.api.client.json.GenericJson; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.internal.NonNull; @@ -41,7 +45,9 @@ public class TopicManagementResponse { private final int successCount; private final List errors; - TopicManagementResponse(List> results) { + TopicManagementResponse(List results) { + checkArgument(results != null && !results.isEmpty(), + "unexpected response from topic management service"); int successCount = 0; ImmutableList.Builder errors = ImmutableList.builder(); for (int i = 0; i < results.size(); i++) { diff --git a/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java b/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java new file mode 100644 index 000000000..d63f3af95 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java @@ -0,0 +1,46 @@ +package com.google.firebase.messaging.internal; + +import com.google.api.client.json.GenericJson; +import com.google.api.client.util.Key; +import com.google.firebase.internal.Nullable; +import java.util.List; +import java.util.Map; + +/** + * The DTO for parsing error responses from the FCM service. + */ +public class MessagingServiceErrorResponse extends GenericJson { + + private static final String FCM_ERROR_TYPE = + "type.googleapis.com/google.firebase.fcm.v1.FcmError"; + + @Key("error") + private Map error; + + @Nullable + public String getErrorCode() { + if (error == null) { + return null; + } + Object details = error.get("details"); + if (details != null && details instanceof List) { + for (Object detail : (List) details) { + if (detail instanceof Map) { + Map detailMap = (Map) detail; + if (FCM_ERROR_TYPE.equals(detailMap.get("@type"))) { + return (String) detailMap.get("errorCode"); + } + } + } + } + return (String) error.get("status"); + } + + @Nullable + public String getErrorMessage() { + if (error != null) { + return (String) error.get("message"); + } + return null; + } +} diff --git a/src/main/java/com/google/firebase/messaging/internal/MessagingServiceResponse.java b/src/main/java/com/google/firebase/messaging/internal/MessagingServiceResponse.java new file mode 100644 index 000000000..b3105ec61 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/internal/MessagingServiceResponse.java @@ -0,0 +1,16 @@ +package com.google.firebase.messaging.internal; + +import com.google.api.client.util.Key; + +/** + * The DTO for parsing success responses from the FCM service. + */ +public class MessagingServiceResponse { + + @Key("name") + private String messageId; + + public String getMessageId() { + return this.messageId; + } +} diff --git a/src/main/java/com/google/firebase/messaging/internal/package-info.java b/src/main/java/com/google/firebase/messaging/internal/package-info.java new file mode 100644 index 000000000..5ef722b72 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/internal/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2019 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. + */ + +/** + * @hide + */ +package com.google.firebase.messaging.internal; diff --git a/src/test/java/com/google/firebase/messaging/BatchResponseTest.java b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java new file mode 100644 index 000000000..cea001085 --- /dev/null +++ b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2019 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; + +public class BatchResponseTest { + + @Test + public void testEmptyResponses() { + List responses = new ArrayList<>(); + + BatchResponse batchResponse = new BatchResponse(responses); + + assertEquals(0, batchResponse.getSuccessCount()); + assertEquals(0, batchResponse.getFailureCount()); + assertEquals(0, batchResponse.getResponses().size()); + } + + @Test + public void testSomeResponse() { + ImmutableList responses = ImmutableList.of( + SendResponse.fromMessageId("message1"), + SendResponse.fromMessageId("message2"), + SendResponse.fromException(new FirebaseMessagingException("error-code", + "error-message", null)) + ); + + BatchResponse batchResponse = new BatchResponse(responses); + + assertEquals(2, batchResponse.getSuccessCount()); + assertEquals(1, batchResponse.getFailureCount()); + assertEquals(3, batchResponse.getResponses().size()); + for (int i = 0; i < 3; i ++) { + assertSame(responses.get(i), batchResponse.getResponses().get(i)); + } + } + + @Test + public void testResponsesImmutable() { + List responses = new ArrayList<>(); + responses.add(SendResponse.fromMessageId("message1")); + BatchResponse batchResponse = new BatchResponse(responses); + SendResponse sendResponse = SendResponse.fromMessageId("message2"); + + try { + batchResponse.getResponses().add(sendResponse); + fail("No error thrown when modifying responses list"); + } catch (UnsupportedOperationException expected) { + // expected + } + } + + @Test(expected = NullPointerException.class) + public void testResponsesCannotBeNull() { + new BatchResponse(null); + } +} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index f309889e8..9a2b3ff83 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -17,10 +17,15 @@ package com.google.firebase.messaging; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import com.google.common.collect.ImmutableList; import com.google.firebase.testing.IntegrationTestUtils; +import java.util.ArrayList; +import java.util.List; import org.junit.BeforeClass; import org.junit.Test; @@ -31,7 +36,7 @@ public class FirebaseMessagingIT { + "PWB1AykXAVUUGl2h1wT4XI6XazWpvY7RBUSYfoxtqSWGIm2nvWh2BOP1YG501SsRoE"; @BeforeClass - public static void setUpClass() throws Exception { + public static void setUpClass() { IntegrationTestUtils.ensureDefaultApp(); } @@ -61,6 +66,91 @@ public void testSend() throws Exception { assertTrue(id != null && id.matches("^projects/.*/messages/.*$")); } + @Test + public void testSendAll() throws Exception { + List messages = new ArrayList<>(); + messages.add( + Message.builder() + .setNotification(new Notification("Title", "Body")) + .setTopic("foo-bar") + .build()); + messages.add( + Message.builder() + .setNotification(new Notification("Title", "Body")) + .setTopic("foo-bar") + .build()); + messages.add( + Message.builder() + .setNotification(new Notification("Title", "Body")) + .setToken("not-a-token") + .build()); + + BatchResponse response = FirebaseMessaging.getInstance().sendAll(messages, true); + + assertEquals(2, response.getSuccessCount()); + assertEquals(1, response.getFailureCount()); + + List responses = response.getResponses(); + assertEquals(3, responses.size()); + assertTrue(responses.get(0).isSuccessful()); + String id = responses.get(0).getMessageId(); + assertTrue(id != null && id.matches("^projects/.*/messages/.*$")); + + assertTrue(responses.get(1).isSuccessful()); + id = responses.get(1).getMessageId(); + assertTrue(id != null && id.matches("^projects/.*/messages/.*$")); + + assertFalse(responses.get(2).isSuccessful()); + assertNull(responses.get(2).getMessageId()); + FirebaseMessagingException exception = responses.get(2).getException(); + assertNotNull(exception); + assertEquals("invalid-argument", exception.getErrorCode()); + } + + @Test + public void testSendHundred() throws Exception { + List messages = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + messages.add(Message.builder().setTopic("foo-bar-" + (i % 10)).build()); + } + + BatchResponse response = FirebaseMessaging.getInstance().sendAll(messages, true); + + assertEquals(100, response.getResponses().size()); + assertEquals(100, response.getSuccessCount()); + assertEquals(0, response.getFailureCount()); + for (SendResponse sendResponse : response.getResponses()) { + if (!sendResponse.isSuccessful()) { + sendResponse.getException().printStackTrace(); + } + assertTrue(sendResponse.isSuccessful()); + String id = sendResponse.getMessageId(); + assertTrue(id != null && id.matches("^projects/.*/messages/.*$")); + assertNull(sendResponse.getException()); + } + } + + @Test + public void testSendMulticast() throws Exception { + MulticastMessage multicastMessage = MulticastMessage.builder() + .setNotification(new Notification("Title", "Body")) + .addToken("not-a-token") + .addToken("also-not-a-token") + .build(); + + BatchResponse response = FirebaseMessaging.getInstance().sendMulticast( + multicastMessage, true); + + assertEquals(0, response.getSuccessCount()); + assertEquals(2, response.getFailureCount()); + assertEquals(2, response.getResponses().size()); + for (SendResponse sendResponse : response.getResponses()) { + assertFalse(sendResponse.isSuccessful()); + assertNull(sendResponse.getMessageId()); + assertNotNull(sendResponse.getException()); + } + } + @Test public void testSubscribe() throws Exception { FirebaseMessaging messaging = FirebaseMessaging.getInstance(); diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index d18e2be74..3c9b7c4fb 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -17,6 +17,7 @@ package com.google.firebase.messaging; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; @@ -24,8 +25,12 @@ import static org.junit.Assert.fail; import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpRequest; import com.google.api.client.json.JsonParser; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; @@ -35,10 +40,12 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.SdkUtils; 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 com.google.firebase.testing.TestUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigDecimal; @@ -54,18 +61,28 @@ public class FirebaseMessagingTest { private static final String TEST_FCM_URL = "https://fcm.googleapis.com/v1/projects/test-project/messages:send"; + private static final String TEST_IID_SUBSCRIBE_URL = "https://iid.googleapis.com/iid/v1:batchAdd"; + private static final String TEST_IID_UNSUBSCRIBE_URL = "https://iid.googleapis.com/iid/v1:batchRemove"; + private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); + private static final String MOCK_RESPONSE = "{\"name\": \"mock-name\"}"; - private static final ImmutableList.Builder tooManyIds = ImmutableList.builder(); + private static final String MOCK_BATCH_SUCCESS_RESPONSE = TestUtils.loadResource( + "fcm_batch_success.txt"); + + private static final String MOCK_BATCH_FAILURE_RESPONSE = TestUtils.loadResource( + "fcm_batch_failure.txt"); + + private static final ImmutableList.Builder TOO_MANY_IDS = ImmutableList.builder(); static { for (int i = 0; i < 1001; i++) { - tooManyIds.add("id" + i); + TOO_MANY_IDS.add("id" + i); } } @@ -74,7 +91,7 @@ public class FirebaseMessagingTest { new TopicMgtArgs(null, "test-topic"), new TopicMgtArgs(ImmutableList.of(), "test-topic"), new TopicMgtArgs(ImmutableList.of(""), "test-topic"), - new TopicMgtArgs(tooManyIds.build(), "test-topic"), + new TopicMgtArgs(TOO_MANY_IDS.build(), "test-topic"), new TopicMgtArgs(ImmutableList.of(""), null), new TopicMgtArgs(ImmutableList.of("id"), ""), new TopicMgtArgs(ImmutableList.of("id"), "foo*") @@ -85,6 +102,46 @@ public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); } + @Test + public void testGetInstance() { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + FirebaseApp.initializeApp(options); + + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + assertSame(messaging, FirebaseMessaging.getInstance()); + } + + @Test + public void testGetInstanceByApp() { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options, "custom-app"); + + FirebaseMessaging messaging = FirebaseMessaging.getInstance(app); + assertSame(messaging, FirebaseMessaging.getInstance(app)); + } + + @Test + public void testPostDeleteApp() { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options, "custom-app"); + app.delete(); + try { + FirebaseMessaging.getInstance(app); + fail("No error thrown for deleted app"); + } catch (IllegalStateException expected) { + // expected + } + } + @Test public void testNoProjectId() { FirebaseOptions options = new FirebaseOptions.Builder() @@ -100,10 +157,9 @@ public void testNoProjectId() { } @Test - public void testNullMessage() { - FirebaseMessaging messaging = initDefaultMessaging(); + public void testSendNullMessage() { TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); + FirebaseMessaging messaging = initDefaultMessaging(interceptor); try { messaging.sendAsync(null); fail("No error thrown for null message"); @@ -118,7 +174,8 @@ public void testNullMessage() { public void testSend() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() .setContent(MOCK_RESPONSE); - final FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final FirebaseMessaging messaging = initMessaging(response, interceptor); Map> testMessages = buildTestMessages(); List> functions = ImmutableList.of( @@ -138,13 +195,12 @@ public String call(Object... args) throws Exception { for (GenericFunction fn : functions) { for (Map.Entry> entry : testMessages.entrySet()) { response.setContent(MOCK_RESPONSE); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); String resp = fn.call(entry.getKey()); assertEquals("mock-name", resp); - HttpRequest request = checkRequestHeader(interceptor); - checkRequest(request, ImmutableMap.of("message", entry.getValue())); + checkRequestHeader(interceptor.getLastRequest()); + checkRequest(interceptor.getLastRequest(), + ImmutableMap.of("message", entry.getValue())); } } } @@ -153,7 +209,8 @@ public String call(Object... args) throws Exception { public void testSendDryRun() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() .setContent(MOCK_RESPONSE); - final FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final FirebaseMessaging messaging = initMessaging(response, interceptor); Map> testMessages = buildTestMessages(); List> functions = ImmutableList.of( @@ -174,13 +231,12 @@ public String call(Object... args) throws Exception { for (GenericFunction fn : functions) { for (Map.Entry> entry : testMessages.entrySet()) { response.setContent(MOCK_RESPONSE); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); String resp = fn.call(entry.getKey()); assertEquals("mock-name", resp); - HttpRequest request = checkRequestHeader(interceptor); - checkRequest(request, ImmutableMap.of("message", entry.getValue(), "validate_only", true)); + checkRequestHeader(interceptor.getLastRequest()); + checkRequest(interceptor.getLastRequest(), + ImmutableMap.of("message", entry.getValue(), "validate_only", true)); } } } @@ -188,11 +244,10 @@ public String call(Object... args) throws Exception { @Test public void testSendError() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent("{}"); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); try { messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); fail("No error thrown for HTTP error"); @@ -204,18 +259,17 @@ public void testSendError() throws Exception { error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); } - checkRequestHeader(interceptor); + checkRequestHeader(interceptor.getLastRequest()); } } @Test public void testSendErrorWithZeroContentResponse() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setZeroContent(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); try { messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); fail("No error thrown for HTTP error"); @@ -227,19 +281,18 @@ public void testSendErrorWithZeroContentResponse() throws Exception { error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); } - checkRequestHeader(interceptor); + checkRequestHeader(interceptor.getLastRequest()); } } @Test public void testSendErrorWithDetails() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); try { messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); fail("No error thrown for HTTP error"); @@ -250,19 +303,18 @@ public void testSendErrorWithDetails() throws Exception { assertEquals("test error", error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); } - checkRequestHeader(interceptor); + checkRequestHeader(interceptor.getLastRequest()); } } @Test public void testSendErrorWithCanonicalCode() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent( "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}"); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); try { messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); fail("No error thrown for HTTP error"); @@ -273,21 +325,20 @@ public void testSendErrorWithCanonicalCode() throws Exception { assertEquals("test error", error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); } - checkRequestHeader(interceptor); + checkRequestHeader(interceptor.getLastRequest()); } } @Test public void testSendErrorWithFcmError() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); for (int code : HTTP_ERRORS) { response.setStatusCode(code).setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); try { messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); fail("No error thrown for HTTP error"); @@ -298,15 +349,364 @@ public void testSendErrorWithFcmError() throws Exception { assertEquals("test error", error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); } - checkRequestHeader(interceptor); + checkRequestHeader(interceptor.getLastRequest()); } } @Test - public void testInvalidSubscribe() { + public void testSendMulticastWithNull() { FirebaseMessaging messaging = initDefaultMessaging(); + try { + messaging.sendMulticastAsync(null); + fail("No error thrown for null multicast message"); + } catch (NullPointerException expected) { + // expected + } + } + + @Test + public void testSendMulticast() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = getMessagingForBatchRequest( + MOCK_BATCH_SUCCESS_RESPONSE, interceptor); + MulticastMessage multicast = MulticastMessage.builder() + .addToken("token1") + .addToken("token2") + .build(); + + BatchResponse responses = messaging.sendMulticast(multicast); + + assertSendBatchSuccess(responses, interceptor); + } + + @Test + public void testSendMulticastAsync() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = getMessagingForBatchRequest( + MOCK_BATCH_SUCCESS_RESPONSE, interceptor); + MulticastMessage multicast = MulticastMessage.builder() + .addToken("token1") + .addToken("token2") + .build(); + + BatchResponse responses = messaging.sendMulticastAsync(multicast).get(); + + assertSendBatchSuccess(responses, interceptor); + } + + @Test + public void testSendMulticastFailure() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = getMessagingForBatchRequest( + MOCK_BATCH_FAILURE_RESPONSE, interceptor); + MulticastMessage multicast = MulticastMessage.builder() + .addToken("token1") + .addToken("token2") + .addToken("token3") + .build(); + + BatchResponse responses = messaging.sendMulticast(multicast); + + assertSendBatchFailure(responses, interceptor); + } + + @Test + public void testSendMulticastAsyncFailure() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = getMessagingForBatchRequest( + MOCK_BATCH_FAILURE_RESPONSE, interceptor); + MulticastMessage multicast = MulticastMessage.builder() + .addToken("token1") + .addToken("token2") + .addToken("token3") + .build(); + + BatchResponse responses = messaging.sendMulticastAsync(multicast).get(); + + assertSendBatchFailure(responses, interceptor); + } + + @Test + public void testSendAllWithNull() { + FirebaseMessaging messaging = initDefaultMessaging(); + try { + messaging.sendAllAsync(null); + fail("No error thrown for null message list"); + } catch (NullPointerException expected) { + // expected + } + } + + @Test + public void testSendAllWithEmptyList() { + FirebaseMessaging messaging = initDefaultMessaging(); + try { + messaging.sendAllAsync(ImmutableList.of()); + fail("No error thrown for empty message list"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testSendAllWithTooManyMessages() { + FirebaseMessaging messaging = initDefaultMessaging(); + ImmutableList.Builder listBuilder = ImmutableList.builder(); + for (int i = 0; i < 101; i++) { + listBuilder.add(Message.builder().setTopic("topic").build()); + } + try { + messaging.sendAllAsync(listBuilder.build()); + fail("No error thrown for too many messages in the list"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testSendAll() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = getMessagingForBatchRequest( + MOCK_BATCH_SUCCESS_RESPONSE, interceptor); + List messages = ImmutableList.of( + Message.builder().setTopic("topic1").build(), + Message.builder().setTopic("topic2").build() + ); + + BatchResponse responses = messaging.sendAll(messages); + + assertSendBatchSuccess(responses, interceptor); + } + + @Test + public void testSendAllAsync() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = getMessagingForBatchRequest( + MOCK_BATCH_SUCCESS_RESPONSE, interceptor); + List messages = ImmutableList.of( + Message.builder().setTopic("topic1").build(), + Message.builder().setTopic("topic2").build() + ); + + BatchResponse responses = messaging.sendAllAsync(messages).get(); + + assertSendBatchSuccess(responses, interceptor); + } + + @Test + public void testSendAllFailure() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = getMessagingForBatchRequest( + MOCK_BATCH_FAILURE_RESPONSE, interceptor); + List messages = ImmutableList.of( + Message.builder().setTopic("topic1").build(), + Message.builder().setTopic("topic2").build(), + Message.builder().setTopic("topic3").build() + ); + + BatchResponse responses = messaging.sendAll(messages); + + assertSendBatchFailure(responses, interceptor); + } + + @Test + public void testSendAllAsyncFailure() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = getMessagingForBatchRequest( + MOCK_BATCH_FAILURE_RESPONSE, interceptor); + List messages = ImmutableList.of( + Message.builder().setTopic("topic1").build(), + Message.builder().setTopic("topic2").build(), + Message.builder().setTopic("topic3").build() + ); + + BatchResponse responses = messaging.sendAllAsync(messages).get(); + + assertSendBatchFailure(responses, interceptor); + } + + @Test + public void testSendAllHttpError() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); + List messages = ImmutableList.of(Message.builder() + .setTopic("test-topic") + .build()); + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent("{}"); + try { + messaging.sendAllAsync(messages).get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("unknown-error", error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: " + code + "; body: {}", + error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllTransportError() throws Exception { + FirebaseMessaging messaging = initFaultyTransportMessaging(); + List messages = ImmutableList.of(Message.builder() + .setTopic("test-topic") + .build()); + + try { + messaging.sendAllAsync(messages).get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling FCM backend service", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test + public void testSendAllErrorWithEmptyResponse() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); + FirebaseMessaging messaging = initMessaging(response, interceptor); + List messages = ImmutableList.of(Message.builder() + .setTopic("test-topic") + .build()); + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setZeroContent(); + + try { + messaging.sendAllAsync(messages).get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("unknown-error", error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: " + code + "; body: null", + error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllErrorWithDetails() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); + List messages = ImmutableList.of(Message.builder() + .setTopic("test-topic") + .build()); + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); + + try { + messaging.sendAllAsync(messages).get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("invalid-argument", error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllErrorWithCanonicalCode() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); + List messages = ImmutableList.of(Message.builder() + .setTopic("test-topic") + .build()); + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}"); + + try { + messaging.sendAllAsync(messages).get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("registration-token-not-registered", error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllErrorWithFcmError() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); + List messages = ImmutableList.of(Message.builder() + .setTopic("test-topic") + .build()); + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " + + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); + + try { + messaging.sendAllAsync(messages).get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("registration-token-not-registered", error.getErrorCode()); + assertEquals("test error", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllErrorWithoutMessage() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); + List messages = ImmutableList.of(Message.builder() + .setTopic("test-topic") + .build()); + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " + + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); + + try { + messaging.sendAllAsync(messages).get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("registration-token-not-registered", error.getErrorCode()); + assertTrue(error.getMessage().startsWith("Unexpected HTTP response")); + assertTrue(error.getCause() instanceof HttpResponseException); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testInvalidSubscribe() { + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initDefaultMessaging(interceptor); for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { try { @@ -324,7 +724,8 @@ public void testInvalidSubscribe() { public void testSubscribe() throws Exception { final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - final FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final FirebaseMessaging messaging = initMessaging(response, interceptor); List> functions = ImmutableList.of( new GenericFunction() { @@ -350,23 +751,21 @@ public TopicManagementResponse call(Object... args) throws Exception { ); for (GenericFunction fn : functions) { - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); response.setContent(responseString); TopicManagementResponse result = fn.call(); - HttpRequest request = checkTopicManagementRequestHeader(interceptor, TEST_IID_SUBSCRIBE_URL); - checkTopicManagementRequest(request, result); + checkTopicManagementRequestHeader( + interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + checkTopicManagementRequest(interceptor.getLastRequest(), result); } } @Test public void testSubscribeError() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); for (int statusCode : HTTP_ERRORS) { response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); try { messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); fail("No error thrown for HTTP error"); @@ -378,15 +777,49 @@ public void testSubscribeError() throws Exception { assertTrue(error.getCause() instanceof HttpResponseException); } - checkTopicManagementRequestHeader(interceptor, TEST_IID_SUBSCRIBE_URL); + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + } + } + + @Test + public void testSubscribeUnknownError() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); + response.setStatusCode(500).setContent("{}"); + try { + messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500; body: {}", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + } + + @Test + public void testSubscribeTransportError() throws Exception { + FirebaseMessaging messaging = initFaultyTransportMessaging(); + try { + messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling IID backend service", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); } } @Test public void testInvalidUnsubscribe() { - FirebaseMessaging messaging = initDefaultMessaging(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); + FirebaseMessaging messaging = initDefaultMessaging(interceptor); for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { try { @@ -404,7 +837,8 @@ public void testInvalidUnsubscribe() { public void testUnsubscribe() throws Exception { final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - final FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final FirebaseMessaging messaging = initMessaging(response, interceptor); List> functions = ImmutableList.of( new GenericFunction() { @@ -430,24 +864,21 @@ public TopicManagementResponse call(Object... args) throws Exception { ); for (GenericFunction fn : functions) { - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); response.setContent(responseString); TopicManagementResponse result = fn.call(); - HttpRequest request = checkTopicManagementRequestHeader( - interceptor, TEST_IID_UNSUBSCRIBE_URL); - checkTopicManagementRequest(request, result); + checkTopicManagementRequestHeader( + interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + checkTopicManagementRequest(interceptor.getLastRequest(), result); } } @Test public void testUnsubscribeError() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - FirebaseMessaging messaging = initMessaging(response); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); for (int statusCode : HTTP_ERRORS) { response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - messaging.setInterceptor(interceptor); try { messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); fail("No error thrown for HTTP error"); @@ -459,19 +890,47 @@ public void testUnsubscribeError() throws Exception { assertTrue(error.getCause() instanceof HttpResponseException); } - checkTopicManagementRequestHeader(interceptor, TEST_IID_UNSUBSCRIBE_URL); + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); } } - private static String getTopicManagementErrorCode(int statusCode) { - String code = FirebaseMessaging.IID_ERROR_CODES.get(statusCode); - if (code == null) { - code = "unknown-error"; + @Test + public void testUnsubscribeUnknownError() throws Exception { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessaging messaging = initMessaging(response, interceptor); + response.setStatusCode(500).setContent("{}"); + try { + messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500; body: {}", error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + } + + @Test + public void testUnsubscribeTransportError() throws Exception { + FirebaseMessaging messaging = initFaultyTransportMessaging(); + try { + messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseMessagingException); + FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling IID backend service", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); } - return code; } - private static FirebaseMessaging initMessaging(MockLowLevelHttpResponse mockResponse) { + private static FirebaseMessaging initMessaging( + MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) { MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(mockResponse) .build(); @@ -482,21 +941,33 @@ private static FirebaseMessaging initMessaging(MockLowLevelHttpResponse mockResp .build(); FirebaseApp app = FirebaseApp.initializeApp(options); - FirebaseMessaging messaging = FirebaseMessaging.getInstance(); - assertSame(messaging, FirebaseMessaging.getInstance(app)); - return messaging; + return new FirebaseMessaging(app, interceptor); } private static FirebaseMessaging initDefaultMessaging() { + return initDefaultMessaging(null); + } + + private static FirebaseMessaging initDefaultMessaging(HttpResponseInterceptor interceptor) { FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); FirebaseApp app = FirebaseApp.initializeApp(options); - return FirebaseMessaging.getInstance(app); + return new FirebaseMessaging(app, interceptor); } - private static void checkRequest( + private static FirebaseMessaging initFaultyTransportMessaging() { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(new FailingHttpTransport()) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + return new FirebaseMessaging(app, null); + } + + private void checkRequest( HttpRequest request, Map expected) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); request.getContent().writeTo(out); @@ -506,17 +977,108 @@ private static void checkRequest( assertEquals(expected, parsed); } - private static HttpRequest checkRequestHeader(TestResponseInterceptor interceptor) { - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); + private void checkRequestHeader(HttpRequest request) { assertEquals("POST", request.getRequestMethod()); assertEquals(TEST_FCM_URL, request.getUrl().toString()); + HttpHeaders headers = request.getHeaders(); + assertEquals("Bearer test-token", headers.getAuthorization()); + assertEquals("2", headers.get("X-GOOG-API-FORMAT-VERSION")); + assertEquals("Java/Admin/" + SdkUtils.getVersion(), headers.get("X-Client-Version")); + } + + private FirebaseMessaging getMessagingForBatchRequest( + String responsePayload, TestResponseInterceptor interceptor) { + MockLowLevelHttpResponse httpResponse = new MockLowLevelHttpResponse() + .setContentType("multipart/mixed; boundary=test_boundary") + .setContent(responsePayload); + return initMessaging(httpResponse, interceptor); + } + + private void assertSendBatchSuccess( + BatchResponse batchResponse, TestResponseInterceptor interceptor) throws IOException { + + assertEquals(2, batchResponse.getSuccessCount()); + assertEquals(0, batchResponse.getFailureCount()); + + List responses = batchResponse.getResponses(); + assertEquals(2, responses.size()); + for (int i = 0; i < 2; i++) { + SendResponse sendResponse = responses.get(i); + assertTrue(sendResponse.isSuccessful()); + assertEquals("projects/test-project/messages/" + (i + 1), sendResponse.getMessageId()); + assertNull(sendResponse.getException()); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + checkBatchRequest(interceptor.getLastRequest(), 2); + } + + private void assertSendBatchFailure( + BatchResponse batchResponse, TestResponseInterceptor interceptor) throws IOException { + + assertEquals(1, batchResponse.getSuccessCount()); + assertEquals(2, batchResponse.getFailureCount()); + + List responses = batchResponse.getResponses(); + assertEquals(3, responses.size()); + SendResponse firstResponse = responses.get(0); + assertTrue(firstResponse.isSuccessful()); + assertEquals("projects/test-project/messages/1", firstResponse.getMessageId()); + assertNull(firstResponse.getException()); + + SendResponse secondResponse = responses.get(1); + assertFalse(secondResponse.isSuccessful()); + assertNull(secondResponse.getMessageId()); + FirebaseMessagingException exception = secondResponse.getException(); + assertNotNull(exception); + assertEquals("invalid-argument", exception.getErrorCode()); + + SendResponse thirdResponse = responses.get(2); + assertFalse(thirdResponse.isSuccessful()); + assertNull(thirdResponse.getMessageId()); + exception = thirdResponse.getException(); + assertNotNull(exception); + assertEquals("invalid-argument", exception.getErrorCode()); + + checkBatchRequestHeader(interceptor.getLastRequest()); + checkBatchRequest(interceptor.getLastRequest(), 3); + } + + private void checkBatchRequestHeader(HttpRequest request) { + assertEquals("POST", request.getRequestMethod()); + assertEquals("https://fcm.googleapis.com/batch", request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - assertEquals("2", request.getHeaders().get("X-GOOG-API-FORMAT-VERSION")); - return request; } - private static void checkTopicManagementRequest( + private void checkBatchRequest(HttpRequest request, int expectedParts) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + request.getContent().writeTo(out); + String[] lines = out.toString().split("\n"); + assertEquals(expectedParts, countLinesWithPrefix(lines, "POST " + TEST_FCM_URL)); + assertEquals(expectedParts, countLinesWithPrefix(lines, "x-goog-api-format-version: 2")); + assertEquals(expectedParts, countLinesWithPrefix( + lines, "x-client-version: Java/Admin/" + SdkUtils.getVersion())); + } + + private int countLinesWithPrefix(String[] lines, String prefix) { + int matchCount = 0; + for (String line : lines) { + if (line.trim().startsWith(prefix)) { + matchCount++; + } + } + return matchCount; + } + + private static String getTopicManagementErrorCode(int statusCode) { + String code = InstanceIdClient.IID_ERROR_CODES.get(statusCode); + if (code == null) { + code = "unknown-error"; + } + return code; + } + + + private void checkTopicManagementRequest( HttpRequest request, TopicManagementResponse result) throws IOException { assertEquals(1, result.getSuccessCount()); assertEquals(1, result.getFailureCount()); @@ -534,14 +1096,11 @@ private static void checkTopicManagementRequest( assertEquals(ImmutableList.of("id1", "id2"), parsed.get("registration_tokens")); } - private static HttpRequest checkTopicManagementRequestHeader( - TestResponseInterceptor interceptor, String expectedUrl) { - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); + private void checkTopicManagementRequestHeader( + HttpRequest request, String expectedUrl) { assertEquals("POST", request.getRequestMethod()); assertEquals(expectedUrl, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - return request; } private static class TopicMgtArgs { @@ -554,6 +1113,13 @@ private static class TopicMgtArgs { } } + private static class FailingHttpTransport extends HttpTransport { + @Override + protected LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + throw new IOException("transport error"); + } + } + private static Map> buildTestMessages() { ImmutableMap.Builder> builder = ImmutableMap.builder(); @@ -660,7 +1226,7 @@ private static Map> buildTestMessages() { "payload", ImmutableMap.of("k1", "v1", "k2", true, "aps", ImmutableMap.of("badge", new BigDecimal(42), "alert", ImmutableMap.of( - "title", "test-title", "subtitle", "test-subtitle", + "title", "test-title", "subtitle", "test-subtitle", "body", "test-body")))) )); diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 5b53b1a4b..788127da8 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -17,6 +17,7 @@ package com.google.firebase.messaging; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; import com.google.api.client.googleapis.util.Utils; @@ -561,6 +562,27 @@ public void testInvalidApnsConfig() { } } + @Test + public void testWrapForTransportDryRun() { + Message message = Message.builder() + .setTopic("foo") + .build(); + Map wrappedMessage = message.wrapForTransport(true); + assertEquals(2, wrappedMessage.size()); + assertSame(message, wrappedMessage.get("message")); + assertEquals(Boolean.TRUE, wrappedMessage.get("validate_only")); + } + + @Test + public void testWrapForTransport() { + Message message = Message.builder() + .setTopic("foo") + .build(); + Map wrappedMessage = message.wrapForTransport(false); + assertEquals(1, wrappedMessage.size()); + assertSame(message, wrappedMessage.get("message")); + } + private static void assertJsonEquals( Map expected, Object actual) throws IOException { assertEquals(expected, toMap(actual)); @@ -574,5 +596,4 @@ private static Map toMap(Object object) throws IOException { parser.parse(map); return map; } - } diff --git a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java new file mode 100644 index 000000000..5cd053103 --- /dev/null +++ b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2019 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.List; +import org.junit.Test; + +public class MulticastMessageTest { + + private static final AndroidConfig ANDROID = AndroidConfig.builder() + .setCollapseKey("collapseKey") + .build(); + private static final ApnsConfig APNS = ApnsConfig.builder() + .setAps(Aps.builder() + .setBadge(42) + .build()) + .build(); + private static final WebpushConfig WEBPUSH = WebpushConfig.builder() + .putData("key", "value") + .build(); + private static final Notification NOTIFICATION = new Notification("title", "body"); + + @Test + public void testMulticastMessage() { + MulticastMessage multicastMessage = MulticastMessage.builder() + .setAndroidConfig(ANDROID) + .setApnsConfig(APNS) + .setWebpushConfig(WEBPUSH) + .setNotification(NOTIFICATION) + .putData("key1", "value1") + .putAllData(ImmutableMap.of("key2", "value2")) + .addToken("token1") + .addAllTokens(ImmutableList.of("token2", "token3")) + .build(); + + List messages = multicastMessage.getMessageList(); + + assertEquals(3, messages.size()); + for (int i = 0; i < 3; i++) { + Message message = messages.get(i); + assertMessage(message, "token" + (i + 1)); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNoTokens() { + MulticastMessage.builder().build(); + } + + @Test + public void testTooManyTokens() { + MulticastMessage.Builder builder = MulticastMessage.builder(); + for (int i = 0; i < 101; i++) { + builder.addToken("token" + i); + } + try { + builder.build(); + fail("No error thrown for more than 100 tokens"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test(expected = NullPointerException.class) + public void testNullToken() { + MulticastMessage.builder().addToken(null).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyToken() { + MulticastMessage.builder().addToken("").build(); + } + + private void assertMessage(Message message, String expectedToken) { + assertSame(ANDROID, message.getAndroidConfig()); + assertSame(APNS, message.getApnsConfig()); + assertSame(WEBPUSH, message.getWebpushConfig()); + assertSame(NOTIFICATION, message.getNotification()); + assertEquals(ImmutableMap.of("key1", "value1", "key2", "value2"), message.getData()); + assertEquals(expectedToken, message.getToken()); + } +} \ No newline at end of file diff --git a/src/test/java/com/google/firebase/messaging/SendResponseTest.java b/src/test/java/com/google/firebase/messaging/SendResponseTest.java new file mode 100644 index 000000000..e9667cc77 --- /dev/null +++ b/src/test/java/com/google/firebase/messaging/SendResponseTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class SendResponseTest { + + @Test + public void testSuccessfulResponse() { + SendResponse response = SendResponse.fromMessageId("message-id"); + + assertEquals("message-id", response.getMessageId()); + assertTrue(response.isSuccessful()); + assertNull(response.getException()); + } + + @Test + public void testFailureResponse() { + FirebaseMessagingException exception = new FirebaseMessagingException("error-code", + "error-message", null); + SendResponse response = SendResponse.fromException(exception); + + assertNull(response.getMessageId()); + assertFalse(response.isSuccessful()); + assertSame(exception, response.getException()); + } + + @Test(expected = IllegalArgumentException.class) + public void testMessageIdCannotBeNull() { + SendResponse.fromMessageId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testMessageIdCannotBeEmpty() { + SendResponse.fromMessageId(""); + } + + @Test(expected = NullPointerException.class) + public void testExceptionCannotBeNull() { + SendResponse.fromException(null); + } +} diff --git a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java index 316e2062f..7c46ad9ac 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java @@ -21,13 +21,17 @@ import com.google.firebase.messaging.ApnsConfig; import com.google.firebase.messaging.Aps; import com.google.firebase.messaging.ApsAlert; +import com.google.firebase.messaging.BatchResponse; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.MulticastMessage; import com.google.firebase.messaging.Notification; +import com.google.firebase.messaging.SendResponse; import com.google.firebase.messaging.TopicManagementResponse; import com.google.firebase.messaging.WebpushConfig; import com.google.firebase.messaging.WebpushNotification; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -113,6 +117,82 @@ public void sendDryRun() throws FirebaseMessagingException { // [END send_dry_run] } + public void sendAll() throws FirebaseMessagingException { + String registrationToken = "YOUR_REGISTRATION_TOKEN"; + + // [START send_all] + // Create a list containing up to 100 messages. + List messages = Arrays.asList( + Message.builder() + .setNotification(new Notification("Price drop", "5% off all electronics")) + .setToken(registrationToken) + .build(), + // ... + Message.builder() + .setNotification(new Notification("Price drop", "2% off all books")) + .setTopic("readers-club") + .build() + ); + + BatchResponse response = FirebaseMessaging.getInstance().sendAll(messages); + // See the BatchResponse reference documentation + // for the contents of response. + System.out.println(response.getSuccessCount() + " messages were sent successfully"); + // [END send_all] + } + + public void sendMulticast() throws FirebaseMessagingException { + // [START send_multicast] + // Create a list containing up to 100 registration tokens. + // These registration tokens come from the client FCM SDKs. + List registrationTokens = Arrays.asList( + "YOUR_REGISTRATION_TOKEN_1", + // ... + "YOUR_REGISTRATION_TOKEN_n" + ); + + MulticastMessage message = MulticastMessage.builder() + .putData("score", "850") + .putData("time", "2:45") + .addAllTokens(registrationTokens) + .build(); + BatchResponse response = FirebaseMessaging.getInstance().sendMulticast(message); + // See the BatchResponse reference documentation + // for the contents of response. + System.out.println(response.getSuccessCount() + " messages were sent successfully"); + // [END send_multicast] + } + + public void sendMulticastAndHandleErrors() throws FirebaseMessagingException { + // [START send_multicast_error] + // These registration tokens come from the client FCM SDKs. + List registrationTokens = Arrays.asList( + "YOUR_REGISTRATION_TOKEN_1", + // ... + "YOUR_REGISTRATION_TOKEN_n" + ); + + MulticastMessage message = MulticastMessage.builder() + .putData("score", "850") + .putData("time", "2:45") + .addAllTokens(registrationTokens) + .build(); + BatchResponse response = FirebaseMessaging.getInstance().sendMulticast(message); + if (response.getFailureCount() > 0) { + List responses = response.getResponses(); + List failedTokens = new ArrayList<>(); + for (int i = 0; i < responses.size(); i++) { + if (!responses.get(i).isSuccessful()) { + // The order of responses corresponds to the order of the registration tokens. + failedTokens.add(registrationTokens.get(i)); + } + } + + System.out.println("List of tokens that caused failures: " + failedTokens); + } + // [END send_multicast_error] + } + public Message androidMessage() { // [START android_message] Message message = Message.builder() diff --git a/src/test/java/com/google/firebase/testing/TestResponseInterceptor.java b/src/test/java/com/google/firebase/testing/TestResponseInterceptor.java index 5daa6c3ee..7bc3a7ec6 100644 --- a/src/test/java/com/google/firebase/testing/TestResponseInterceptor.java +++ b/src/test/java/com/google/firebase/testing/TestResponseInterceptor.java @@ -16,6 +16,9 @@ package com.google.firebase.testing; +import static org.junit.Assert.assertNotNull; + +import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseInterceptor; import java.io.IOException; @@ -28,11 +31,16 @@ public class TestResponseInterceptor implements HttpResponseInterceptor { private HttpResponse response; @Override - public void interceptResponse(HttpResponse response) throws IOException { + public void interceptResponse(HttpResponse response) { this.response = response; } public HttpResponse getResponse() { return response; } + + public HttpRequest getLastRequest() { + assertNotNull(response); + return response.getRequest(); + } } diff --git a/src/test/resources/fcm_batch_failure.txt b/src/test/resources/fcm_batch_failure.txt new file mode 100644 index 000000000..c4fb04307 --- /dev/null +++ b/src/test/resources/fcm_batch_failure.txt @@ -0,0 +1,80 @@ +--test_boundary +Content-Type: application/http +Content-ID: response-1 + +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Vary: Origin +Vary: X-Origin +Vary: Referer + +{ + "name": "projects/test-project/messages/1" +} + +--test_boundary +Content-Type: application/http +Content-ID: response-2 + +HTTP/1.1 400 Bad Request +Vary: Origin +Vary: X-Origin +Vary: Referer +Content-Type: application/json; charset=UTF-8 + +{ + "error": { + "code": 400, + "message": "The registration token is not a valid FCM registration token", + "status": "INVALID_ARGUMENT", + "details": [ + { + "@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", + "errorCode": "INVALID_ARGUMENT" + }, + { + "@type": "type.googleapis.com/google.rpc.BadRequest", + "fieldViolations": [ + { + "field": "message.token", + "description": "The registration token is not a valid FCM registration token" + } + ] + } + ] + } +} + +--test_boundary +Content-Type: application/http +Content-ID: response-3 + +HTTP/1.1 400 Bad Request +Vary: Origin +Vary: X-Origin +Vary: Referer +Content-Type: application/json; charset=UTF-8 + +{ + "error": { + "code": 400, + "status": "INVALID_ARGUMENT", + "details": [ + { + "@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", + "errorCode": "INVALID_ARGUMENT" + }, + { + "@type": "type.googleapis.com/google.rpc.BadRequest", + "fieldViolations": [ + { + "field": "message.token", + "description": "The registration token is not a valid FCM registration token" + } + ] + } + ] + } +} + +--test_boundary-- \ No newline at end of file diff --git a/src/test/resources/fcm_batch_success.txt b/src/test/resources/fcm_batch_success.txt new file mode 100644 index 000000000..e318e97c6 --- /dev/null +++ b/src/test/resources/fcm_batch_success.txt @@ -0,0 +1,29 @@ +--test_boundary +Content-Type: application/http +Content-ID: response-1 + +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Vary: Origin +Vary: X-Origin +Vary: Referer + +{ + "name": "projects/test-project/messages/1" +} + +--test_boundary +Content-Type: application/http +Content-ID: response-2 + +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Vary: Origin +Vary: X-Origin +Vary: Referer + +{ + "name": "projects/test-project/messages/2" +} + +--test_boundary-- \ No newline at end of file From f338383cb4ac340a9067a777f442c58caa65f7dc Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 14 Mar 2019 13:12:59 -0700 Subject: [PATCH 055/456] Staging release 6.8.0 (#260) * Updating CHANGELOG for 6.8.0 release. * [maven-release-plugin] prepare release v6.8.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 e669a2855..cea3b25ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +- + +# v6.8.0 + - [added] Implemented new `sendAll()` and `sendMulticast()` APIs in `FirebaseMessaging`. - [changed] Removed org.json dependency and replaced with com.google.code.gson. diff --git a/pom.xml b/pom.xml index 0fcfe205a..27e137a8a 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.7.1-SNAPSHOT + 6.8.1-SNAPSHOT jar firebase-admin From 4a3f6f62a3a99fab205d6d6a8720732a90cac223 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 14 Mar 2019 15:31:26 -0700 Subject: [PATCH 056/456] Refactoring the FirebaseAuth Token Verification (#246) * Refactoring the token verification logic * Added more tests * Cleaning up the unit tests * Implemented lazy init for token verifiers * Using Supplier interface instead of the custom LazyInitializer * Made the FirebaseAuth more testable * Removed FirebaseIdToken * More unit tests for token verification * Decoupled FirebaseTokenVerifier from IdToken API * Decoupled FirebaseToken from IdToken API * Added some documentation * reordering some methods for clarity * Fixed a comment typo * Minor readability improvements * Added tests to verify default token verifiers in FirebaseAuth * Making args of all op() methods final --- .../google/firebase/auth/FirebaseAuth.java | 224 +++++---- .../google/firebase/auth/FirebaseToken.java | 138 +---- .../firebase/auth/FirebaseTokenUtils.java | 123 +++++ .../firebase/auth/FirebaseTokenVerifier.java | 34 ++ .../auth/FirebaseTokenVerifierImpl.java | 328 ++++++++++++ .../firebase/auth/FirebaseUserManager.java | 2 - .../auth/RevocationCheckDecorator.java | 80 +++ .../firebase/auth/internal/CryptoSigners.java | 4 +- .../auth/internal/FirebaseTokenFactory.java | 11 +- .../auth/internal/FirebaseTokenVerifier.java | 335 ------------ .../firebase/auth/internal/KeyManagers.java | 85 ---- .../google/firebase/auth/FirebaseAuthIT.java | 6 +- .../firebase/auth/FirebaseAuthTest.java | 363 ++++++++++++- .../firebase/auth/FirebaseTokenTest.java | 122 +++++ .../firebase/auth/FirebaseTokenUtilsTest.java | 130 +++++ .../auth/FirebaseTokenVerifierImplTest.java | 340 +++++++++++++ .../TestOnlyImplFirebaseAuthTrampolines.java | 43 -- .../firebase/auth/TestTokenFactory.java | 89 ++++ .../internal/FirebaseTokenVerifierTest.java | 476 ------------------ 19 files changed, 1746 insertions(+), 1187 deletions(-) create mode 100644 src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java create mode 100644 src/main/java/com/google/firebase/auth/FirebaseTokenVerifier.java create mode 100644 src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java create mode 100644 src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java delete mode 100644 src/main/java/com/google/firebase/auth/internal/FirebaseTokenVerifier.java delete mode 100644 src/main/java/com/google/firebase/auth/internal/KeyManagers.java create mode 100644 src/test/java/com/google/firebase/auth/FirebaseTokenTest.java create mode 100644 src/test/java/com/google/firebase/auth/FirebaseTokenUtilsTest.java create mode 100644 src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java delete mode 100644 src/test/java/com/google/firebase/auth/TestOnlyImplFirebaseAuthTrampolines.java create mode 100644 src/test/java/com/google/firebase/auth/TestTokenFactory.java delete mode 100644 src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 843c42f1f..0e02f8371 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -25,6 +25,8 @@ import com.google.api.core.ApiFuture; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; @@ -34,17 +36,15 @@ import com.google.firebase.auth.UserRecord.CreateRequest; import com.google.firebase.auth.UserRecord.UpdateRequest; import com.google.firebase.auth.internal.FirebaseTokenFactory; -import com.google.firebase.auth.internal.FirebaseTokenVerifier; -import com.google.firebase.auth.internal.KeyManagers; import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; + import java.io.IOException; 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. @@ -56,40 +56,27 @@ */ public class FirebaseAuth { + private static final String SERVICE_ID = FirebaseAuth.class.getName(); + private static final String ERROR_CUSTOM_TOKEN = "ERROR_CUSTOM_TOKEN"; - private static final String ERROR_INVALID_ID_TOKEN = "ERROR_INVALID_CREDENTIAL"; - private static final String ERROR_INVALID_SESSION_COOKIE = "ERROR_INVALID_COOKIE"; - private final Clock clock; + private final Object lock = new Object(); + private final AtomicBoolean destroyed = new AtomicBoolean(false); private final FirebaseApp firebaseApp; - private final KeyManagers keyManagers; - private final String projectId; + private final Supplier tokenFactory; + private final Supplier idTokenVerifier; + private final Supplier cookieVerifier; private final JsonFactory jsonFactory; private final FirebaseUserManager userManager; - private final AtomicReference tokenFactory; - private final AtomicBoolean destroyed; - private final Object lock; - - private FirebaseAuth(FirebaseApp firebaseApp) { - this(firebaseApp, KeyManagers.getDefault(firebaseApp, Clock.SYSTEM), Clock.SYSTEM); - } - /** - * Constructor for injecting a GooglePublicKeysManager, which is used to verify tokens are - * correctly signed. This should only be used for testing to override the default key manager. - */ - @VisibleForTesting - FirebaseAuth(FirebaseApp firebaseApp, KeyManagers keyManagers, Clock clock) { - this.firebaseApp = checkNotNull(firebaseApp); - this.keyManagers = checkNotNull(keyManagers); - this.clock = checkNotNull(clock); - this.projectId = ImplFirebaseTrampolines.getProjectId(firebaseApp); + private FirebaseAuth(Builder builder) { + this.firebaseApp = checkNotNull(builder.firebaseApp); + this.tokenFactory = threadSafeMemoize(builder.tokenFactory); + this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier); + this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier); this.jsonFactory = firebaseApp.getOptions().getJsonFactory(); this.userManager = new FirebaseUserManager(firebaseApp); - this.tokenFactory = new AtomicReference<>(null); - this.destroyed = new AtomicBoolean(false); - this.lock = new Object(); } /** @@ -224,40 +211,23 @@ public ApiFuture verifySessionCookieAsync(String cookie, boolean private CallableOperation verifySessionCookieOp( final String cookie, final boolean checkRevoked) { checkNotDestroyed(); - checkState(!Strings.isNullOrEmpty(projectId), - "Must initialize FirebaseApp with a project ID to call verifySessionCookie()"); + checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty"); + final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked); return new CallableOperation() { @Override public FirebaseToken execute() throws FirebaseAuthException { - FirebaseTokenVerifier firebaseTokenVerifier = - FirebaseTokenVerifier.createSessionCookieVerifier(projectId, keyManagers, clock); - FirebaseToken firebaseToken; - try { - firebaseToken = FirebaseToken.parse(jsonFactory, cookie); - } catch (IOException e) { - throw new FirebaseAuthException(ERROR_INVALID_SESSION_COOKIE, - "Failed to parse cookie", e); - } - // This will throw a FirebaseAuthException with details on how the token is invalid. - firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken()); - - if (checkRevoked) { - checkRevoked(firebaseToken, "session cookie", - FirebaseUserManager.SESSION_COOKIE_REVOKED_ERROR); - } - return firebaseToken; + return sessionCookieVerifier.verifyToken(cookie); } }; } - private void checkRevoked( - FirebaseToken firebaseToken, String label, String errorCode) throws FirebaseAuthException { - String uid = firebaseToken.getUid(); - UserRecord user = userManager.getUserById(uid); - long issuedAt = (long) firebaseToken.getClaims().get("iat"); - if (user.getTokensValidAfterTimestamp() > issuedAt * 1000) { - throw new FirebaseAuthException(errorCode, "Firebase " + label + " revoked"); + @VisibleForTesting + FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) { + FirebaseTokenVerifier verifier = cookieVerifier.get(); + if (checkRevoked) { + verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager); } + return verifier; } /** @@ -355,7 +325,7 @@ private CallableOperation createCustomTokenOp( final String uid, final Map developerClaims) { checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - final FirebaseTokenFactory tokenFactory = ensureTokenFactory(); + final FirebaseTokenFactory tokenFactory = this.tokenFactory.get(); return new CallableOperation() { @Override public String execute() throws FirebaseAuthException { @@ -369,29 +339,6 @@ 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. * @@ -472,31 +419,24 @@ private CallableOperation verifyIdTokenOp( final String token, final boolean checkRevoked) { checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(token), "ID token must not be null or empty"); - checkArgument(!Strings.isNullOrEmpty(projectId), - "Must initialize FirebaseApp with a project ID to call verifyIdToken()"); + final FirebaseTokenVerifier verifier = getIdTokenVerifier(checkRevoked); return new CallableOperation() { @Override protected FirebaseToken execute() throws FirebaseAuthException { - FirebaseTokenVerifier firebaseTokenVerifier = - FirebaseTokenVerifier.createIdTokenVerifier(projectId, keyManagers, clock); - FirebaseToken firebaseToken; - try { - firebaseToken = FirebaseToken.parse(jsonFactory, token); - } catch (IOException e) { - throw new FirebaseAuthException(ERROR_INVALID_ID_TOKEN, "Failed to parse token", e); - } - - // This will throw a FirebaseAuthException with details on how the token is invalid. - firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken()); - - if (checkRevoked) { - checkRevoked(firebaseToken, "auth token", FirebaseUserManager.ID_TOKEN_REVOKED_ERROR); - } - return firebaseToken; + return verifier.verifyToken(token); } }; } + @VisibleForTesting + FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) { + FirebaseTokenVerifier verifier = idTokenVerifier.get(); + if (checkRevoked) { + verifier = RevocationCheckDecorator.decorateIdTokenVerifier(verifier, userManager); + } + return verifier; + } + /** * Revokes all refresh tokens for the specified user. * @@ -705,13 +645,12 @@ public ApiFuture listUsersAsync(@Nullable String pageToken) { * @throws IllegalArgumentException If the specified page token is empty, or max results value * is invalid. */ - public ApiFuture listUsersAsync( - @Nullable final String pageToken, final int maxResults) { + public ApiFuture listUsersAsync(@Nullable String pageToken, int maxResults) { return listUsersOp(pageToken, maxResults).callAsync(firebaseApp); } private CallableOperation listUsersOp( - @Nullable String pageToken, int maxResults) { + @Nullable final String pageToken, final int maxResults) { checkNotDestroyed(); final PageFactory factory = new PageFactory( new DefaultUserSource(userManager, jsonFactory), maxResults, pageToken); @@ -874,7 +813,7 @@ public void deleteUser(@NonNull String uid) throws FirebaseAuthException { * {@link FirebaseAuthException}. * @throws IllegalArgumentException If the user ID string is null or empty. */ - public ApiFuture deleteUserAsync(final String uid) { + public ApiFuture deleteUserAsync(String uid) { return deleteUserOp(uid).callAsync(firebaseApp); } @@ -959,7 +898,7 @@ public ApiFuture importUsersAsync(List users } private CallableOperation importUsersOp( - List users, UserImportOptions options) { + final List users, final UserImportOptions options) { checkNotDestroyed(); final UserImportRequest request = new UserImportRequest(users, options, jsonFactory); return new CallableOperation() { @@ -1130,6 +1069,11 @@ public ApiFuture generateSignInWithEmailLinkAsync( .callAsync(firebaseApp); } + @VisibleForTesting + FirebaseUserManager getUserManager() { + return this.userManager; + } + private CallableOperation generateEmailActionLinkOp( final EmailLinkType type, final String email, final ActionCodeSettings settings) { checkNotDestroyed(); @@ -1145,9 +1089,17 @@ protected String execute() throws FirebaseAuthException { }; } - @VisibleForTesting - FirebaseUserManager getUserManager() { - return this.userManager; + private Supplier threadSafeMemoize(final Supplier supplier) { + checkNotNull(supplier); + return Suppliers.memoize(new Supplier() { + @Override + public T get() { + synchronized (lock) { + checkNotDestroyed(); + return supplier.get(); + } + } + }); } private void checkNotDestroyed() { @@ -1163,12 +1115,72 @@ private void destroy() { } } - private static final String SERVICE_ID = FirebaseAuth.class.getName(); + private static FirebaseAuth fromApp(final FirebaseApp app) { + return FirebaseAuth.builder() + .setFirebaseApp(app) + .setTokenFactory(new Supplier() { + @Override + public FirebaseTokenFactory get() { + return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM); + } + }) + .setIdTokenVerifier(new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM); + } + }) + .setCookieVerifier(new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); + } + }) + .build(); + } + + @VisibleForTesting + static Builder builder() { + return new Builder(); + } + + static class Builder { + private FirebaseApp firebaseApp; + private Supplier tokenFactory; + private Supplier idTokenVerifier; + private Supplier cookieVerifier; + + private Builder() { } + + Builder setFirebaseApp(FirebaseApp firebaseApp) { + this.firebaseApp = firebaseApp; + return this; + } + + Builder setTokenFactory(Supplier tokenFactory) { + this.tokenFactory = tokenFactory; + return this; + } + + Builder setIdTokenVerifier(Supplier idTokenVerifier) { + this.idTokenVerifier = idTokenVerifier; + return this; + } + + Builder setCookieVerifier(Supplier cookieVerifier) { + this.cookieVerifier = cookieVerifier; + return this; + } + + FirebaseAuth build() { + return new FirebaseAuth(this); + } + } private static class FirebaseAuthService extends FirebaseService { FirebaseAuthService(FirebaseApp app) { - super(SERVICE_ID, new FirebaseAuth(app)); + super(SERVICE_ID, FirebaseAuth.fromApp(app)); } @Override diff --git a/src/main/java/com/google/firebase/auth/FirebaseToken.java b/src/main/java/com/google/firebase/auth/FirebaseToken.java index 1726250a4..3d7b0b254 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseToken.java +++ b/src/main/java/com/google/firebase/auth/FirebaseToken.java @@ -16,164 +16,64 @@ package com.google.firebase.auth; -import com.google.api.client.auth.openidconnect.IdToken; -import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.webtoken.JsonWebSignature; -import com.google.api.client.util.Key; +import static com.google.common.base.Preconditions.checkArgument; -import java.io.IOException; +import com.google.common.collect.ImmutableMap; import java.util.Map; /** - * Implementation of a Parsed Firebase Token returned by {@link FirebaseAuth#verifyIdToken(String)}. - * It can used to get the uid and other attributes of the user provided in the Token. + * A decoded and verified Firebase token. Can be used to get the uid and other user attributes + * available in the token. See {@link FirebaseAuth#verifyIdToken(String)} and + * {@link FirebaseAuth#verifySessionCookie(String)} for details on how to obtain an instance of + * this class. */ public final class FirebaseToken { - private final FirebaseTokenImpl token; + private final Map claims; - FirebaseToken(FirebaseTokenImpl token) { - this.token = token; - } - - static FirebaseToken parse(JsonFactory jsonFactory, String tokenString) throws IOException { - try { - JsonWebSignature jws = - JsonWebSignature.parser(jsonFactory) - .setPayloadClass(FirebaseTokenImpl.Payload.class) - .parse(tokenString); - return new FirebaseToken( - new FirebaseTokenImpl( - jws.getHeader(), - (FirebaseTokenImpl.Payload) jws.getPayload(), - jws.getSignatureBytes(), - jws.getSignedContentBytes())); - } catch (IOException e) { - throw new IOException( - "Decoding Firebase ID token failed. Make sure you passed the entire string JWT " - + "which represents an ID token. See https://firebase.google.com/docs/auth/admin/" - + "verify-id-tokens for details on how to retrieve an ID token.", - e); - } + FirebaseToken(Map claims) { + checkArgument(claims != null && claims.containsKey("sub"), + "Claims map must at least contain sub"); + this.claims = ImmutableMap.copyOf(claims); } /** Returns the Uid for the this token. */ public String getUid() { - return token.getPayload().getSubject(); + return (String) claims.get("sub"); } /** Returns the Issuer for the this token. */ public String getIssuer() { - return token.getPayload().getIssuer(); + return (String) claims.get("iss"); } /** Returns the user's display name. */ public String getName() { - return token.getPayload().getName(); + return (String) claims.get("name"); } /** Returns the Uri string of the user's profile photo. */ public String getPicture() { - return token.getPayload().getPicture(); + return (String) claims.get("picture"); } /** * Returns the e-mail address for this user, or {@code null} if it's unavailable. */ public String getEmail() { - return token.getPayload().getEmail(); + return (String) claims.get("email"); } /** * Indicates if the email address returned by {@link #getEmail()} has been verified as good. */ public boolean isEmailVerified() { - return token.getPayload().isEmailVerified(); + Object emailVerified = claims.get("email_verified"); + return emailVerified != null && (Boolean) emailVerified; } /** Returns a map of all of the claims on this token. */ public Map getClaims() { - return token.getPayload(); - } - - FirebaseTokenImpl getToken() { - return token; - } - - static class FirebaseTokenImpl extends IdToken { - - FirebaseTokenImpl( - Header header, Payload payload, byte[] signatureBytes, byte[] signedContentBytes) { - super(header, payload, signatureBytes, signedContentBytes); - } - - @Override - public Payload getPayload() { - return (Payload) super.getPayload(); - } - - /** Represents a FirebaseWebToken Payload. */ - public static class Payload extends IdToken.Payload { - - /** - * Timestamp of the last time this user authenticated with Firebase on the device receiving - * this token. - */ - @Key("auth_time") - private long authTime; - - /** User's primary email address. */ - @Key private String email; - - /** Indicates whether or not the e-mail field is verified to be a known-good address. */ - @Key("email_verified") - private boolean emailVerified; - - /** User's Display Name. */ - @Key private String name; - - /** URI of the User's profile picture. */ - @Key private String picture; - - /** - * Returns the UID of the user represented by this token. This is an alias for {@link - * #getSubject()} - */ - public String getUid() { - return getSubject(); - } - - /** - * Returns the time in seconds from the Unix Epoch that this user last authenticated with - * Firebase on this device. - */ - public long getAuthTime() { - return authTime; - } - - /** - * Returns the e-mail address for this user, or {@code null} if it's unavailable. - */ - public String getEmail() { - return email; - } - - /** - * Indicates if the email address returned by {@link #getEmail()} has been verified as good. - */ - public boolean isEmailVerified() { - return emailVerified; - } - - /** Returns the user's display name. */ - public String getName() { - return name; - } - - /** Returns the Uri string of the user's profile photo. */ - public String getPicture() { - return picture; - } - } + return this.claims; } } diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java new file mode 100644 index 000000000..dbb562872 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java @@ -0,0 +1,123 @@ +/* + * Copyright 2019 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.checkState; + +import com.google.api.client.auth.openidconnect.IdTokenVerifier; +import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.util.Clock; +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; +import com.google.firebase.auth.internal.CryptoSigners; +import com.google.firebase.auth.internal.FirebaseTokenFactory; + +import java.io.IOException; + +final class FirebaseTokenUtils { + + private static final String ID_TOKEN_CERT_URL = + "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"; + private static final String ID_TOKEN_ISSUER_PREFIX = "https://securetoken.google.com/"; + + private static final String SESSION_COOKIE_CERT_URL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys"; + private static final String SESSION_COOKIE_ISSUER_PREFIX = "https://session.firebase.google.com/"; + + // The default JsonFactory implementation we get from Google API client does not support parsing + // JSON strings with control characters in text. The public key certificates we get from Google + // auth servers contain some control characters, and therefore we must use a JsonFactory that is + // capable of parsing such text. + static final JsonFactory UNQUOTED_CTRL_CHAR_JSON_FACTORY = new GsonFactory(); + + private FirebaseTokenUtils() { } + + static FirebaseTokenFactory createTokenFactory(FirebaseApp firebaseApp, Clock clock) { + try { + return new FirebaseTokenFactory( + firebaseApp.getOptions().getJsonFactory(), + clock, + CryptoSigners.getCryptoSigner(firebaseApp)); + } 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); + } + } + + static FirebaseTokenVerifierImpl createIdTokenVerifier(FirebaseApp app, Clock clock) { + String projectId = ImplFirebaseTrampolines.getProjectId(app); + checkState(!Strings.isNullOrEmpty(projectId), + "Must initialize FirebaseApp with a project ID to call verifyIdToken()"); + IdTokenVerifier idTokenVerifier = newIdTokenVerifier( + clock, ID_TOKEN_ISSUER_PREFIX, projectId); + GooglePublicKeysManager publicKeysManager = newPublicKeysManager( + app.getOptions(), clock, ID_TOKEN_CERT_URL); + return FirebaseTokenVerifierImpl.builder() + .setShortName("ID token") + .setMethod("verifyIdToken()") + .setDocUrl("https://firebase.google.com/docs/auth/admin/verify-id-tokens") + .setJsonFactory(app.getOptions().getJsonFactory()) + .setPublicKeysManager(publicKeysManager) + .setIdTokenVerifier(idTokenVerifier) + .build(); + } + + static FirebaseTokenVerifierImpl createSessionCookieVerifier(FirebaseApp app, Clock clock) { + String projectId = ImplFirebaseTrampolines.getProjectId(app); + checkState(!Strings.isNullOrEmpty(projectId), + "Must initialize FirebaseApp with a project ID to call verifySessionCookie()"); + IdTokenVerifier idTokenVerifier = newIdTokenVerifier( + clock, SESSION_COOKIE_ISSUER_PREFIX, projectId); + GooglePublicKeysManager publicKeysManager = newPublicKeysManager( + app.getOptions(), clock, SESSION_COOKIE_CERT_URL); + return FirebaseTokenVerifierImpl.builder() + .setJsonFactory(app.getOptions().getJsonFactory()) + .setPublicKeysManager(publicKeysManager) + .setIdTokenVerifier(idTokenVerifier) + .setShortName("session cookie") + .setMethod("verifySessionCookie()") + .setDocUrl("https://firebase.google.com/docs/auth/admin/manage-cookies") + .build(); + } + + private static GooglePublicKeysManager newPublicKeysManager( + FirebaseOptions options, Clock clock, String certUrl) { + return new GooglePublicKeysManager.Builder( + options.getHttpTransport(), UNQUOTED_CTRL_CHAR_JSON_FACTORY) + .setClock(clock) + .setPublicCertsEncodedUrl(certUrl) + .build(); + } + + private static IdTokenVerifier newIdTokenVerifier( + Clock clock, String issuerPrefix, String projectId) { + return new IdTokenVerifier.Builder() + .setClock(clock) + .setAudience(ImmutableList.of(projectId)) + .setIssuer(issuerPrefix + projectId) + .build(); + } +} diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifier.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifier.java new file mode 100644 index 000000000..e73f82381 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifier.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019 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; + +/** + * An interface for verifying Firebase token strings. Exists mainly to facilitate easy testing + * and extension/decoration of the token verification functionality. + */ +interface FirebaseTokenVerifier { + + /** + * Verifies that the given token string is a valid Firebase JWT. + * + * @param token The token string to be verified. + * @return A decoded representation of the input token string. + * @throws FirebaseAuthException If the input token string fails to verify due to any reason. + */ + FirebaseToken verifyToken(String token) throws FirebaseAuthException; + +} diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java new file mode 100644 index 000000000..1da2d7a34 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java @@ -0,0 +1,328 @@ +/* + * Copyright 2019 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.auth.openidconnect.IdToken; +import com.google.api.client.auth.openidconnect.IdToken.Payload; +import com.google.api.client.auth.openidconnect.IdTokenVerifier; +import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.webtoken.JsonWebSignature.Header; +import com.google.api.client.util.ArrayMap; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import java.io.IOException; +import java.math.BigDecimal; +import java.security.GeneralSecurityException; +import java.security.PublicKey; + +/** + * The default implementation of the {@link FirebaseTokenVerifier} interface. Uses the Google API + * client's {@code IdToken} API to decode and verify token strings. Can be customized to verify + * both Firebase ID tokens and session cookies. + */ +final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier { + + private static final String RS256 = "RS256"; + private static final String FIREBASE_AUDIENCE = + "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"; + private static final String ERROR_INVALID_CREDENTIAL = "ERROR_INVALID_CREDENTIAL"; + private static final String ERROR_RUNTIME_EXCEPTION = "ERROR_RUNTIME_EXCEPTION"; + + private final JsonFactory jsonFactory; + private final GooglePublicKeysManager publicKeysManager; + private final IdTokenVerifier idTokenVerifier; + private final String method; + private final String shortName; + private final String articledShortName; + private final String docUrl; + + private FirebaseTokenVerifierImpl(Builder builder) { + this.jsonFactory = checkNotNull(builder.jsonFactory); + this.publicKeysManager = checkNotNull(builder.publicKeysManager); + this.idTokenVerifier = checkNotNull(builder.idTokenVerifier); + checkArgument(!Strings.isNullOrEmpty(builder.method), "method name must be specified"); + checkArgument(!Strings.isNullOrEmpty(builder.shortName), "shortName must be specified"); + checkArgument(!Strings.isNullOrEmpty(builder.docUrl), "docUrl must be specified"); + this.method = builder.method; + this.shortName = builder.shortName; + this.articledShortName = prefixWithIndefiniteArticle(this.shortName); + this.docUrl = builder.docUrl; + } + + /** + * Verifies that the given token string is a valid Firebase JWT. This implementation considers + * a token string to be valid if all the following conditions are met: + *

        + *
      1. The token string is a valid RS256 JWT.
      2. + *
      3. The JWT contains a valid key ID (kid) claim.
      4. + *
      5. The JWT is not expired, and it has been issued some time in the past.
      6. + *
      7. The JWT contains valid issuer (iss) and audience (aud) claims as determined by the + * {@code IdTokenVerifier}.
      8. + *
      9. The JWT contains a valid subject (sub) claim.
      10. + *
      11. The JWT is signed by a Firebase Auth backend server.
      12. + *
      + * + * @param token The token string to be verified. + * @return A decoded representation of the input token string. + * @throws FirebaseAuthException If the input token string does not meet any of the conditions + * listed above. + */ + @Override + public FirebaseToken verifyToken(String token) throws FirebaseAuthException { + IdToken idToken = parse(token); + checkContents(idToken); + checkSignature(idToken); + return new FirebaseToken(idToken.getPayload()); + } + + GooglePublicKeysManager getPublicKeysManager() { + return publicKeysManager; + } + + IdTokenVerifier getIdTokenVerifier() { + return idTokenVerifier; + } + + String getMethod() { + return method; + } + + String getShortName() { + return shortName; + } + + String getArticledShortName() { + return articledShortName; + } + + String getDocUrl() { + return docUrl; + } + + private String prefixWithIndefiniteArticle(String word) { + if ("aeiouAEIOU".indexOf(word.charAt(0)) < 0) { + return "a " + word; + } else { + return "an " + word; + } + } + + private IdToken parse(String token) throws FirebaseAuthException { + try { + return IdToken.parse(jsonFactory, token); + } catch (IOException e) { + String detailedError = String.format( + "Failed to parse Firebase %s. Make sure you passed a string that represents a complete " + + "and valid JWT. See %s for details on how to retrieve %s.", + shortName, + docUrl, + articledShortName); + throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, detailedError, e); + } + } + + private void checkContents(final IdToken token) throws FirebaseAuthException { + String errorMessage = getErrorIfContentInvalid(token); + if (errorMessage != null) { + String detailedError = String.format("%s %s", errorMessage, getVerifyTokenMessage()); + throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, detailedError); + } + } + + private void checkSignature(IdToken token) throws FirebaseAuthException { + try { + if (!isSignatureValid(token)) { + throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, + String.format( + "Failed to verify the signature of Firebase %s. %s", + shortName, + getVerifyTokenMessage())); + } + } catch (GeneralSecurityException | IOException e) { + throw new FirebaseAuthException( + ERROR_RUNTIME_EXCEPTION, "Error while verifying signature.", e); + } + } + + private String getErrorIfContentInvalid(final IdToken idToken) { + final Header header = idToken.getHeader(); + final Payload payload = idToken.getPayload(); + + String errorMessage = null; + if (header.getKeyId() == null) { + errorMessage = getErrorForTokenWithoutKid(header, payload); + } else if (!RS256.equals(header.getAlgorithm())) { + errorMessage = String.format( + "Firebase %s has incorrect algorithm. Expected \"%s\" but got \"%s\".", + shortName, + RS256, + header.getAlgorithm()); + } else if (!idToken.verifyAudience(idTokenVerifier.getAudience())) { + errorMessage = String.format( + "Firebase %s has incorrect \"aud\" (audience) claim. Expected \"%s\" but got \"%s\". %s", + shortName, + joinWithComma(idTokenVerifier.getAudience()), + joinWithComma(payload.getAudienceAsList()), + getProjectIdMatchMessage()); + } else if (!idToken.verifyIssuer(idTokenVerifier.getIssuers())) { + errorMessage = String.format( + "Firebase %s has incorrect \"iss\" (issuer) claim. Expected \"%s\" but got \"%s\". %s", + shortName, + joinWithComma(idTokenVerifier.getIssuers()), + payload.getIssuer(), + getProjectIdMatchMessage()); + } else if (payload.getSubject() == null) { + errorMessage = String.format( + "Firebase %s has no \"sub\" (subject) claim.", + shortName); + } else if (payload.getSubject().isEmpty()) { + errorMessage = String.format( + "Firebase %s has an empty string \"sub\" (subject) claim.", + shortName); + } else if (payload.getSubject().length() > 128) { + errorMessage = String.format( + "Firebase %s has \"sub\" (subject) claim longer than 128 characters.", + shortName); + } else if (!verifyTimestamps(idToken)) { + errorMessage = String.format( + "Firebase %s has expired or is not yet valid. Get a fresh %s and try again.", + shortName, + shortName); + } + + return errorMessage; + } + + private String getVerifyTokenMessage() { + return String.format( + "See %s for details on how to retrieve %s.", + docUrl, + articledShortName); + } + + /** + * Verifies the cryptographic signature on the FirebaseToken. Can block on a web request to fetch + * the keys if they have expired. + */ + private boolean isSignatureValid(IdToken token) throws GeneralSecurityException, IOException { + for (PublicKey key : publicKeysManager.getPublicKeys()) { + if (token.verifySignature(key)) { + return true; + } + } + return false; + } + + private String getErrorForTokenWithoutKid(IdToken.Header header, IdToken.Payload payload) { + if (isCustomToken(payload)) { + return String.format("%s expects %s, but was given a custom token.", + method, articledShortName); + } else if (isLegacyCustomToken(header, payload)) { + return String.format("%s expects %s, but was given a legacy custom token.", + method, articledShortName); + } + return String.format("Firebase %s has no \"kid\" claim.", shortName); + } + + private String joinWithComma(Iterable strings) { + return Joiner.on(',').join(strings); + } + + private String getProjectIdMatchMessage() { + return String.format( + "Make sure the %s comes from the same Firebase project as the service account used to " + + "authenticate this SDK.", + shortName); + } + + private boolean verifyTimestamps(IdToken token) { + long currentTimeMillis = idTokenVerifier.getClock().currentTimeMillis(); + return token.verifyTime(currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds()); + } + + private boolean isCustomToken(IdToken.Payload payload) { + return FIREBASE_AUDIENCE.equals(payload.getAudience()); + } + + private boolean isLegacyCustomToken(IdToken.Header header, IdToken.Payload payload) { + return "HS256".equals(header.getAlgorithm()) + && new BigDecimal(0).equals(payload.get("v")) + && containsLegacyUidField(payload); + } + + private boolean containsLegacyUidField(IdToken.Payload payload) { + Object dataField = payload.get("d"); + if (dataField instanceof ArrayMap) { + return ((ArrayMap) dataField).get("uid") != null; + } + return false; + } + + static Builder builder() { + return new Builder(); + } + + static final class Builder { + + private JsonFactory jsonFactory; + private GooglePublicKeysManager publicKeysManager; + private String method; + private String shortName; + private IdTokenVerifier idTokenVerifier; + private String docUrl; + + private Builder() { } + + Builder setJsonFactory(JsonFactory jsonFactory) { + this.jsonFactory = jsonFactory; + return this; + } + + Builder setPublicKeysManager(GooglePublicKeysManager publicKeysManager) { + this.publicKeysManager = publicKeysManager; + return this; + } + + Builder setMethod(String method) { + this.method = method; + return this; + } + + Builder setShortName(String shortName) { + this.shortName = shortName; + return this; + } + + Builder setIdTokenVerifier(IdTokenVerifier idTokenVerifier) { + this.idTokenVerifier = idTokenVerifier; + return this; + } + + Builder setDocUrl(String docUrl) { + this.docUrl = docUrl; + return this; + } + + FirebaseTokenVerifierImpl build() { + return new FirebaseTokenVerifierImpl(this); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 7f534fda2..8298499b4 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -65,8 +65,6 @@ class FirebaseUserManager { static final String USER_NOT_FOUND_ERROR = "user-not-found"; static final String INTERNAL_ERROR = "internal-error"; - static final String ID_TOKEN_REVOKED_ERROR = "id-token-revoked"; - static final String SESSION_COOKIE_REVOKED_ERROR = "session-cookie-revoked"; // Map of server-side error codes to SDK error codes. // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors diff --git a/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java b/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java new file mode 100644 index 000000000..e53ad25c4 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Strings; + +/** + * A decorator for adding token revocation checks to an existing {@link FirebaseTokenVerifier}. + */ +class RevocationCheckDecorator implements FirebaseTokenVerifier { + + static final String ID_TOKEN_REVOKED_ERROR = "id-token-revoked"; + static final String SESSION_COOKIE_REVOKED_ERROR = "session-cookie-revoked"; + + private final FirebaseTokenVerifier tokenVerifier; + private final FirebaseUserManager userManager; + private final String errorCode; + private final String shortName; + + private RevocationCheckDecorator( + FirebaseTokenVerifier tokenVerifier, + FirebaseUserManager userManager, + String errorCode, + String shortName) { + this.tokenVerifier = checkNotNull(tokenVerifier); + this.userManager = checkNotNull(userManager); + checkArgument(!Strings.isNullOrEmpty(errorCode)); + checkArgument(!Strings.isNullOrEmpty(shortName)); + this.errorCode = errorCode; + this.shortName = shortName; + } + + /** + * If the wrapped {@link FirebaseTokenVerifier} deems the input token string is valid, checks + * whether the token has been revoked. + */ + @Override + public FirebaseToken verifyToken(String token) throws FirebaseAuthException { + FirebaseToken firebaseToken = tokenVerifier.verifyToken(token); + if (isRevoked(firebaseToken)) { + throw new FirebaseAuthException(errorCode, "Firebase " + shortName + " revoked"); + } + return firebaseToken; + } + + private boolean isRevoked(FirebaseToken firebaseToken) throws FirebaseAuthException { + UserRecord user = userManager.getUserById(firebaseToken.getUid()); + long issuedAtInSeconds = (long) firebaseToken.getClaims().get("iat"); + return user.getTokensValidAfterTimestamp() > issuedAtInSeconds * 1000; + } + + static RevocationCheckDecorator decorateIdTokenVerifier( + FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) { + return new RevocationCheckDecorator( + tokenVerifier, userManager, ID_TOKEN_REVOKED_ERROR, "id token"); + } + + static RevocationCheckDecorator decorateSessionCookieVerifier( + FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) { + return new RevocationCheckDecorator( + tokenVerifier, userManager, SESSION_COOKIE_REVOKED_ERROR, "session cookie"); + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java index 3447f05d9..6ea70b880 100644 --- a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java +++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java @@ -31,7 +31,7 @@ /** * A set of {@link CryptoSigner} implementations and utilities for interacting with them. */ -class CryptoSigners { +public class CryptoSigners { private static final String METADATA_SERVICE_URL = "http://metadata/computeMetadata/v1/instance/service-accounts/default/email"; @@ -128,7 +128,7 @@ public static class SignBlobResponse { * 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 { + public 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. 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 72ef91791..95d313134 100644 --- a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java +++ b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java @@ -27,7 +27,6 @@ import com.google.api.client.util.StringUtils; import com.google.common.base.Strings; -import com.google.firebase.FirebaseApp; import java.io.IOException; import java.util.Collection; import java.util.Map; @@ -43,7 +42,7 @@ public class FirebaseTokenFactory { private final Clock clock; private final CryptoSigner signer; - FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) { + public FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) { this.jsonFactory = checkNotNull(jsonFactory); this.clock = checkNotNull(clock); this.signer = checkNotNull(signer); @@ -94,12 +93,4 @@ private String signPayload(JsonWebSignature.Header header, String signature = Base64.encodeBase64URLSafeString(signer.sign(contentBytes)); return content + "." + signature; } - - public static FirebaseTokenFactory fromApp( - FirebaseApp firebaseApp, Clock clock) throws IOException { - return new FirebaseTokenFactory( - firebaseApp.getOptions().getJsonFactory(), - clock, - CryptoSigners.getCryptoSigner(firebaseApp)); - } } diff --git a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenVerifier.java b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenVerifier.java deleted file mode 100644 index 98b269f0a..000000000 --- a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenVerifier.java +++ /dev/null @@ -1,335 +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.auth.internal; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.api.client.auth.openidconnect.IdToken; -import com.google.api.client.auth.openidconnect.IdToken.Payload; -import com.google.api.client.auth.openidconnect.IdTokenVerifier; -import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; -import com.google.api.client.json.webtoken.JsonWebSignature.Header; -import com.google.api.client.util.ArrayMap; -import com.google.api.client.util.Clock; -import com.google.common.base.Strings; -import com.google.firebase.auth.FirebaseAuthException; - -import com.google.firebase.internal.NonNull; -import java.io.IOException; -import java.math.BigDecimal; -import java.security.GeneralSecurityException; -import java.security.PublicKey; -import java.util.Collection; -import java.util.Collections; - -/** - * Verifies that a JWT returned by Firebase is valid for use in the this project. - * - *

      This class should be kept as a Singleton within the server in order to maximize caching of the - * public signing keys. - */ -public final class FirebaseTokenVerifier extends IdTokenVerifier { - - static final String ID_TOKEN_CERT_URL = - "https://www.googleapis.com/robot/v1/metadata/x509/" - + "securetoken@system.gserviceaccount.com"; - static final String ID_TOKEN_ISSUER_PREFIX = "https://securetoken.google.com/"; - - static final String SESSION_COOKIE_CERT_URL = - "https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys"; - static final String SESSION_COOKIE_ISSUER_PREFIX = "https://session.firebase.google.com/"; - - private static final String FIREBASE_AUDIENCE = - "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"; - - private static final String ERROR_INVALID_CREDENTIAL = "ERROR_INVALID_CREDENTIAL"; - private static final String ERROR_RUNTIME_EXCEPTION = "ERROR_RUNTIME_EXCEPTION"; - private static final String PROJECT_ID_MATCH_MESSAGE = - "Make sure the %s comes from the same Firebase project as the service account used to " - + "authenticate this SDK."; - private static final String VERIFY_TOKEN_DOCS_MESSAGE = - "See %s for details on how to retrieve %s."; - private static final String ALGORITHM = "RS256"; - - private final String projectId; - private final GooglePublicKeysManager publicKeysManager; - private final String method; - private final String shortName; - private final String articledShortName; - - private final String projectIdMatchMessage; - private final String verifyTokenMessage; - - private FirebaseTokenVerifier(Builder builder) { - super(builder); - checkArgument(!Strings.isNullOrEmpty(builder.projectId), "projectId must be set"); - checkArgument(!Strings.isNullOrEmpty(builder.shortName), "shortName must be set"); - checkArgument(!Strings.isNullOrEmpty(builder.method), "method must be set"); - this.projectId = builder.projectId; - this.shortName = builder.shortName; - this.articledShortName = prefixWithIndefiniteArticle(shortName); - this.method = builder.method; - this.publicKeysManager = checkNotNull(builder.publicKeysManager, - "publicKeysManager must be set"); - this.projectIdMatchMessage = String.format(PROJECT_ID_MATCH_MESSAGE, shortName); - this.verifyTokenMessage = String.format(VERIFY_TOKEN_DOCS_MESSAGE, builder.docUrl, - articledShortName); - } - - /** - * We are changing the semantics of the super-class method in order to provide more details on why - * this is failing to the developer. - */ - public void verifyTokenAndSignature(IdToken token) throws FirebaseAuthException { - Payload payload = token.getPayload(); - Header header = token.getHeader(); - String errorMessage = null; - - boolean isCustomToken = - payload.getAudience() != null && payload.getAudience().equals(FIREBASE_AUDIENCE); - boolean isLegacyCustomToken = - header.getAlgorithm() != null - && header.getAlgorithm().equals("HS256") - && payload.get("v") != null - && payload.get("v").equals(new BigDecimal(0)) - && payload.get("d") != null - && payload.get("d") instanceof ArrayMap - && ((ArrayMap) payload.get("d")).get("uid") != null; - - if (header.getKeyId() == null) { - if (isCustomToken) { - errorMessage = String.format("%s expects %s, but was given a custom token.", - method, articledShortName); - } else if (isLegacyCustomToken) { - errorMessage = String.format("%s expects %s, but was given a legacy custom token.", - method, articledShortName); - } else { - errorMessage = String.format("Firebase %s has no \"kid\" claim.", shortName); - } - } else if (header.getAlgorithm() == null || !header.getAlgorithm().equals(ALGORITHM)) { - errorMessage = - String.format( - "Firebase %s has incorrect algorithm. Expected \"%s\" but got \"%s\".", - shortName, ALGORITHM, header.getAlgorithm()); - } else if (!token.verifyAudience(getAudience())) { - errorMessage = - String.format( - "Firebase %s has incorrect \"aud\" (audience) claim. Expected \"%s\" but got " - + "\"%s\". %s", shortName, concat(getAudience()), - concat(token.getPayload().getAudienceAsList()), projectIdMatchMessage); - } else if (!token.verifyIssuer(getIssuers())) { - errorMessage = - String.format( - "Firebase %s has incorrect \"iss\" (issuer) claim. " - + "Expected \"%s\" but got \"%s\". %s", shortName, concat(getIssuers()), - token.getPayload().getIssuer(), projectIdMatchMessage); - } else if (payload.getSubject() == null) { - errorMessage = String.format("Firebase %s has no \"sub\" (subject) claim.", shortName); - } else if (payload.getSubject().isEmpty()) { - errorMessage = String.format("Firebase %s has an empty string \"sub\" (subject) claim.", - shortName); - } else if (payload.getSubject().length() > 128) { - errorMessage = String.format("Firebase %s has \"sub\" (subject) claim longer than " - + "128 characters.", shortName); - } else if (!token.verifyTime(getClock().currentTimeMillis(), getAcceptableTimeSkewSeconds())) { - errorMessage = - String.format("Firebase %s has expired or is not yet valid. Get a fresh %s and " - + "try again.", shortName, shortName); - } - - if (errorMessage != null) { - throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, - String.format("%s %s", errorMessage, verifyTokenMessage)); - } - - try { - if (!verifySignature(token)) { - throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, - String.format("Firebase %s isn't signed by a valid public key. %s", - shortName, verifyTokenMessage)); - } - } catch (IOException | GeneralSecurityException e) { - throw new FirebaseAuthException( - ERROR_RUNTIME_EXCEPTION, "Error while verifying signature.", e); - } - } - - private String prefixWithIndefiniteArticle(String word) { - if ("aeiouAEIOU".indexOf(word.charAt(0)) < 0) { - return "a " + word; - } else { - return "an " + word; - } - } - - private String concat(Collection collection) { - StringBuilder stringBuilder = new StringBuilder(); - for (String inputLine : collection) { - stringBuilder.append(inputLine.trim()).append(", "); - } - return stringBuilder.substring(0, stringBuilder.length() - 2); - } - - /** - * Verifies the cryptographic signature on the FirebaseToken. Can block on a web request to fetch - * the keys if they have expired. - */ - private boolean verifySignature(IdToken token) throws GeneralSecurityException, IOException { - for (PublicKey key : publicKeysManager.getPublicKeys()) { - if (token.verifySignature(key)) { - return true; - } - } - return false; - } - - public String getProjectId() { - return projectId; - } - - /** - * Builder for {@link FirebaseTokenVerifier}. - */ - public static class Builder extends IdTokenVerifier.Builder { - - private String projectId; - private String shortName; - private String method; - private String docUrl; - private GooglePublicKeysManager publicKeysManager; - - public String getProjectId() { - return projectId; - } - - /** - * Sets the project ID and a URL prefix for the issuer (iss) claim. The full issuer claim - * is the concatenation of the prefix and the project ID. - * - * @param issuerPrefix A URL prefix. - * @param projectId A Firebase project ID. - * @return This builder. - */ - public Builder setProjectId(String issuerPrefix, String projectId) { - this.projectId = projectId; - this.setAudience(Collections.singleton(projectId)); - this.setIssuer(issuerPrefix + projectId); - return this; - } - - /** - * Sets the short name of the type of tokens being validated (e.g. ID token, session cookie). - * - * @param shortName A short string identifier. - * @return This builder. - */ - public Builder setShortName(String shortName) { - this.shortName = shortName; - return this; - } - - /** - * Sets the name of the operation that triggers token verification (e.g. verifyIdToken()) - * - * @param method A method name. - * @return This builder. - */ - public Builder setMethod(String method) { - this.method = method; - return this; - } - - /** - * A URL to public documentation where more information about token verification can be found. - * - * @param docUrl A documentation URL. - * @return This builder. - */ - public Builder setDocUrl(String docUrl) { - this.docUrl = docUrl; - return this; - } - - /** - * Sets the {@code Clock} instance to be used to compare token issue and expiry times. - * - * @param clock A {@code Clock} instance. - * @return This builder. - */ - @Override - public Builder setClock(Clock clock) { - return (Builder) super.setClock(clock); - } - - /** - * Overrides the GooglePublicKeysManager from the default. - * - * @param publicKeysManager A public keys manager. - * @return This builder. - */ - public Builder setPublicKeysManager(GooglePublicKeysManager publicKeysManager) { - this.publicKeysManager = publicKeysManager; - return this; - } - - @Override - public FirebaseTokenVerifier build() { - return new FirebaseTokenVerifier(this); - } - } - - /** - * Creates a new {@link FirebaseTokenVerifier} for verifying Firebase ID tokens. - * - * @param projectId Project ID string - * @param keyManagers {@link KeyManagers} instance with public key managers to use - * @param clock {@code Clock} instance for Google API client - * @return A new {@link FirebaseTokenVerifier} instance - */ - @NonNull public static FirebaseTokenVerifier createIdTokenVerifier( - @NonNull String projectId, @NonNull KeyManagers keyManagers, @NonNull Clock clock) { - return new FirebaseTokenVerifier.Builder() - .setProjectId(ID_TOKEN_ISSUER_PREFIX, projectId) - .setPublicKeysManager(keyManagers.getIdTokenKeysManager()) - .setShortName("ID token") - .setMethod("verifyIdToken()") - .setDocUrl("https://firebase.google.com/docs/auth/admin/verify-id-tokens") - .setClock(clock) - .build(); - } - - /** - * Creates a new {@link FirebaseTokenVerifier} for verifying Firebase ID tokens. - * - * @param projectId Project ID string - * @param keyManagers {@link KeyManagers} instance with public key managers to use - * @param clock {@code Clock} instance for Google API client - * @return A new {@link FirebaseTokenVerifier} instance - */ - @NonNull public static FirebaseTokenVerifier createSessionCookieVerifier( - @NonNull String projectId, @NonNull KeyManagers keyManagers, @NonNull Clock clock) { - return new FirebaseTokenVerifier.Builder() - .setProjectId(SESSION_COOKIE_ISSUER_PREFIX, projectId) - .setPublicKeysManager(keyManagers.getSessionCookieKeysManager()) - .setShortName("session cookie") - .setMethod("verifySessionCookie()") - .setDocUrl("https://firebase.google.com/docs/auth/admin/manage-cookies") - .setClock(clock) - .build(); - } -} diff --git a/src/main/java/com/google/firebase/auth/internal/KeyManagers.java b/src/main/java/com/google/firebase/auth/internal/KeyManagers.java deleted file mode 100644 index b22837a1a..000000000 --- a/src/main/java/com/google/firebase/auth/internal/KeyManagers.java +++ /dev/null @@ -1,85 +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.auth.internal; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.json.gson.GsonFactory; -import com.google.api.client.util.Clock; -import com.google.common.annotations.VisibleForTesting; -import com.google.firebase.FirebaseApp; -import com.google.firebase.internal.NonNull; - -/** - * A utility for initializing and keeping tack of the various public key manager instances - * used by {@link com.google.firebase.auth.FirebaseAuth}. - */ -public class KeyManagers { - - private final GooglePublicKeysManager idTokenKeysManager; - private final GooglePublicKeysManager sessionCookieKeysManager; - - private KeyManagers( - GooglePublicKeysManager idTokenKeysManager, - GooglePublicKeysManager sessionCookieKeysManager) { - this.idTokenKeysManager = checkNotNull(idTokenKeysManager); - this.sessionCookieKeysManager = checkNotNull(sessionCookieKeysManager); - } - - /** - * Returns the key manager that should be used for ID token verification. - */ - GooglePublicKeysManager getIdTokenKeysManager() { - return idTokenKeysManager; - } - - /** - * Returns the key manager that should be used for session cookie verification. - */ - GooglePublicKeysManager getSessionCookieKeysManager() { - return sessionCookieKeysManager; - } - - /** - * Initialize a new set of key managers for the specified app using the given clock. - * - * @param app A {@link FirebaseApp} instance. - * @param clock A {@code Clock} to be used with Google API client. - * @return A new {@link KeyManagers} instance. - */ - public static KeyManagers getDefault(@NonNull FirebaseApp app, @NonNull Clock clock) { - HttpTransport transport = app.getOptions().getHttpTransport(); - return getDefault(transport, clock); - } - - @VisibleForTesting - static KeyManagers getDefault(HttpTransport transport, Clock clock) { - return new KeyManagers( - createPublicKeysManager(transport, clock, FirebaseTokenVerifier.ID_TOKEN_CERT_URL), - createPublicKeysManager(transport, clock, FirebaseTokenVerifier.SESSION_COOKIE_CERT_URL)); - } - - private static GooglePublicKeysManager createPublicKeysManager( - HttpTransport transport, Clock clock, String certUrl) { - return new GooglePublicKeysManager.Builder(transport, new GsonFactory()) - .setClock(clock) - .setPublicCertsEncodedUrl(certUrl) - .build(); - } -} diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index b01eeb9b9..badb9ead6 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -79,7 +79,7 @@ public class FirebaseAuthIT { "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 final String ACTION_LINK_CONTINUE_URL = "http://localhost/?a=1&b=2#c=3"; private static FirebaseAuth auth; @@ -406,7 +406,7 @@ public void testVerifyIdToken() throws Exception { fail("expecting exception"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.ID_TOKEN_REVOKED_ERROR, + assertEquals(RevocationCheckDecorator.ID_TOKEN_REVOKED_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } idToken = signInWithCustomToken(customToken); @@ -440,7 +440,7 @@ public void testVerifySessionCookie() throws Exception { fail("expecting exception"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.SESSION_COOKIE_REVOKED_ERROR, + assertEquals(RevocationCheckDecorator.SESSION_COOKIE_REVOKED_ERROR, ((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 8f19e5cac..c495c41df 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -16,6 +16,7 @@ package com.google.firebase.auth; +import static com.google.common.base.Preconditions.checkNotNull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; @@ -25,9 +26,13 @@ import com.google.api.core.ApiFuture; import com.google.common.base.Defaults; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +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.internal.FirebaseTokenFactory; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; import java.lang.reflect.InvocationTargetException; @@ -38,10 +43,10 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; public class FirebaseAuthTest { @@ -50,11 +55,6 @@ public class FirebaseAuthTest { .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .build(); - @Before - public void setup() { - FirebaseApp.initializeApp(firebaseOptions); - } - @After public void cleanup() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -62,6 +62,7 @@ public void cleanup() { @Test public void testGetInstance() { + FirebaseApp.initializeApp(firebaseOptions); FirebaseAuth defaultAuth = FirebaseAuth.getInstance(); assertNotNull(defaultAuth); assertSame(defaultAuth, FirebaseAuth.getInstance()); @@ -164,4 +165,354 @@ public void testAuthExceptionNullErrorCode() { public void testAuthExceptionEmptyErrorCode() { new FirebaseAuthException("", "test"); } + + @Test + public void testDefaultIdTokenVerifier() { + FirebaseApp.initializeApp(firebaseOptions); + + FirebaseTokenVerifier tokenVerifier = FirebaseAuth.getInstance() + .getIdTokenVerifier(false); + + assertTrue(tokenVerifier instanceof FirebaseTokenVerifierImpl); + String shortName = ((FirebaseTokenVerifierImpl) tokenVerifier).getShortName(); + assertEquals("ID token", shortName); + } + + @Test + public void testIdTokenVerifierInitializedOnDemand() throws Exception { + FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken("idTokenUser")); + CountingSupplier countingSupplier = new CountingSupplier<>( + Suppliers.ofInstance(tokenVerifier)); + + FirebaseAuth auth = getAuthForIdTokenVerification(countingSupplier); + assertEquals(0, countingSupplier.getCount()); + + auth.verifyIdToken("idToken"); + auth.verifyIdToken("idToken"); + + assertEquals(1, countingSupplier.getCount()); + } + + @Test + public void testVerifyIdTokenWithNull() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); + tokenVerifier.lastTokenString = "_init_"; + FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); + + try { + auth.verifyIdTokenAsync(null); + fail("No error thrown for null id token"); + } catch (IllegalArgumentException expected) { + assertEquals("_init_", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifyIdTokenWithEmptyString() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); + tokenVerifier.lastTokenString = "_init_"; + FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); + + try { + auth.verifyIdTokenAsync(""); + fail("No error thrown for null id token"); + } catch (IllegalArgumentException expected) { + assertEquals("_init_", tokenVerifier.getLastTokenString()); + } + + } + + @Test + public void testVerifyIdToken() throws Exception { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken("testUser")); + FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); + + FirebaseToken firebaseToken = auth.verifyIdToken("idtoken"); + + assertEquals("testUser", firebaseToken.getUid()); + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifyIdTokenFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( + new FirebaseAuthException("TEST_CODE", "Test error message")); + FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); + + try { + auth.verifyIdToken("idtoken"); + fail("No error thrown for invalid token"); + } catch (FirebaseAuthException authException) { + assertEquals("TEST_CODE", authException.getErrorCode()); + assertEquals("Test error message", authException.getMessage()); + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifyIdTokenAsync() throws Exception { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken("testUser")); + FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); + + FirebaseToken firebaseToken = auth.verifyIdTokenAsync("idtoken").get(); + + assertEquals("testUser", firebaseToken.getUid()); + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifyIdTokenAsyncFailure() throws InterruptedException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( + new FirebaseAuthException("TEST_CODE", "Test error message")); + FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); + + try { + auth.verifyIdTokenAsync("idtoken").get(); + fail("No error thrown for invalid token"); + } catch (ExecutionException e) { + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals("TEST_CODE", authException.getErrorCode()); + assertEquals("Test error message", authException.getMessage()); + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifyIdTokenWithCheckRevokedAsyncFailure() throws InterruptedException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( + new FirebaseAuthException("TEST_CODE", "Test error message")); + FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); + + try { + auth.verifyIdTokenAsync("idtoken", true).get(); + fail("No error thrown for invalid token"); + } catch (ExecutionException e) { + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals("TEST_CODE", authException.getErrorCode()); + assertEquals("Test error message", authException.getMessage()); + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testDefaultSessionCookieVerifier() { + FirebaseApp.initializeApp(firebaseOptions); + + FirebaseTokenVerifier tokenVerifier = FirebaseAuth.getInstance() + .getSessionCookieVerifier(false); + + assertTrue(tokenVerifier instanceof FirebaseTokenVerifierImpl); + String shortName = ((FirebaseTokenVerifierImpl) tokenVerifier).getShortName(); + assertEquals("session cookie", shortName); + } + + @Test + public void testSessionCookieVerifierInitializedOnDemand() throws Exception { + FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken("cookieUser")); + CountingSupplier countingSupplier = new CountingSupplier<>( + Suppliers.ofInstance(tokenVerifier)); + FirebaseAuth auth = getAuthForSessionCookieVerification(countingSupplier); + + assertEquals(0, countingSupplier.getCount()); + + auth.verifySessionCookie("sessionCookie"); + auth.verifySessionCookie("sessionCookie"); + + assertEquals(1, countingSupplier.getCount()); + } + + @Test + public void testVerifySessionCookieWithNull() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); + tokenVerifier.lastTokenString = "_init_"; + FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); + + try { + auth.verifySessionCookieAsync(null); + fail("No error thrown for null id token"); + } catch (IllegalArgumentException expected) { + assertEquals("_init_", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifySessionCookieWithEmptyString() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); + tokenVerifier.lastTokenString = "_init_"; + FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); + + try { + auth.verifySessionCookieAsync(""); + fail("No error thrown for null id token"); + } catch (IllegalArgumentException expected) { + assertEquals("_init_", tokenVerifier.getLastTokenString()); + } + + } + + @Test + public void testVerifySessionCookie() throws Exception { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken("testUser")); + FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); + + FirebaseToken firebaseToken = auth.verifySessionCookie("idtoken"); + + assertEquals("testUser", firebaseToken.getUid()); + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifySessionCookieFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( + new FirebaseAuthException("TEST_CODE", "Test error message")); + FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); + + try { + auth.verifySessionCookie("idtoken"); + fail("No error thrown for invalid token"); + } catch (FirebaseAuthException authException) { + assertEquals("TEST_CODE", authException.getErrorCode()); + assertEquals("Test error message", authException.getMessage()); + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifySessionCookieAsync() throws Exception { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken("testUser")); + FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); + + FirebaseToken firebaseToken = auth.verifySessionCookieAsync("idtoken").get(); + + assertEquals("testUser", firebaseToken.getUid()); + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifySessionCookieAsyncFailure() throws InterruptedException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( + new FirebaseAuthException("TEST_CODE", "Test error message")); + FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); + + try { + auth.verifySessionCookieAsync("idtoken").get(); + fail("No error thrown for invalid token"); + } catch (ExecutionException e) { + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals("TEST_CODE", authException.getErrorCode()); + assertEquals("Test error message", authException.getMessage()); + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws InterruptedException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( + new FirebaseAuthException("TEST_CODE", "Test error message")); + FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); + + try { + auth.verifySessionCookieAsync("idtoken", true).get(); + fail("No error thrown for invalid token"); + } catch (ExecutionException e) { + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals("TEST_CODE", authException.getErrorCode()); + assertEquals("Test error message", authException.getMessage()); + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + } + + private FirebaseToken getFirebaseToken(String subject) { + return new FirebaseToken(ImmutableMap.of("sub", subject)); + } + + private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVerifier) { + return getAuthForIdTokenVerification(Suppliers.ofInstance(tokenVerifier)); + } + + private FirebaseAuth getAuthForIdTokenVerification( + Supplier tokenVerifierSupplier) { + FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); + return FirebaseAuth.builder() + .setFirebaseApp(app) + .setTokenFactory(Suppliers.ofInstance(null)) + .setIdTokenVerifier(tokenVerifierSupplier) + .setCookieVerifier(Suppliers.ofInstance(null)) + .build(); + } + + private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) { + return getAuthForSessionCookieVerification(Suppliers.ofInstance(tokenVerifier)); + } + + private FirebaseAuth getAuthForSessionCookieVerification( + Supplier tokenVerifierSupplier) { + FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); + return FirebaseAuth.builder() + .setFirebaseApp(app) + .setTokenFactory(Suppliers.ofInstance(null)) + .setIdTokenVerifier(Suppliers.ofInstance(null)) + .setCookieVerifier(tokenVerifierSupplier) + .build(); + } + + private static class MockTokenVerifier implements FirebaseTokenVerifier { + + private String lastTokenString; + + private FirebaseToken result; + private FirebaseAuthException exception; + + private MockTokenVerifier(FirebaseToken result, FirebaseAuthException exception) { + this.result = result; + this.exception = exception; + } + + @Override + public FirebaseToken verifyToken(String token) throws FirebaseAuthException { + lastTokenString = token; + if (exception != null) { + throw exception; + } + return result; + } + + String getLastTokenString() { + return this.lastTokenString; + } + + static MockTokenVerifier fromResult(FirebaseToken result) { + return new MockTokenVerifier(result, null); + } + + static MockTokenVerifier fromException(FirebaseAuthException exception) { + return new MockTokenVerifier(null, exception); + } + } + + private static class CountingSupplier implements Supplier { + + private final AtomicInteger counter = new AtomicInteger(0); + private final Supplier supplier; + + CountingSupplier(Supplier supplier) { + this.supplier = checkNotNull(supplier); + } + + @Override + public T get() { + counter.incrementAndGet(); + return supplier.get(); + } + + int getCount() { + return counter.get(); + } + } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenTest.java new file mode 100644 index 000000000..af65561a5 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2019 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.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.auth.openidconnect.IdToken; +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import org.junit.Test; + +public class FirebaseTokenTest { + + @Test + public void testFirebaseToken() { + Map claims = ImmutableMap.builder() + .put("sub", "testUser") + .put("iss", "test-project-id") + .put("email", "test@example.com") + .put("email_verified", true) + .put("name", "Test User") + .put("picture", "https://picture.url") + .put("custom", "claim") + .build(); + + FirebaseToken firebaseToken = new FirebaseToken(claims); + + assertEquals("testUser", firebaseToken.getUid()); + assertEquals("test-project-id", firebaseToken.getIssuer()); + assertEquals("test@example.com", firebaseToken.getEmail()); + assertTrue(firebaseToken.isEmailVerified()); + assertEquals("Test User", firebaseToken.getName()); + assertEquals("https://picture.url", firebaseToken.getPicture()); + assertEquals("claim", firebaseToken.getClaims().get("custom")); + assertEquals(7, firebaseToken.getClaims().size()); + } + + @Test + public void testFirebaseTokenMinimal() { + Map claims = ImmutableMap.builder() + .put("sub", "testUser") + .build(); + + FirebaseToken firebaseToken = new FirebaseToken(claims); + + assertEquals("testUser", firebaseToken.getUid()); + assertNull(firebaseToken.getIssuer()); + assertNull(firebaseToken.getEmail()); + assertFalse(firebaseToken.isEmailVerified()); + assertNull(firebaseToken.getName()); + assertNull(firebaseToken.getPicture()); + assertEquals(1, firebaseToken.getClaims().size()); + } + + @Test + public void testFirebaseTokenFromIdToken() { + IdToken.Payload payload = new IdToken.Payload() + .setSubject("testUser") + .setIssuer("test-project-id") + .set("email", "test@example.com") + .set("email_verified", true) + .set("name", "Test User") + .set("picture", "https://picture.url") + .set("custom", "claim"); + + FirebaseToken firebaseToken = new FirebaseToken(payload); + + assertEquals("testUser", firebaseToken.getUid()); + assertEquals("test-project-id", firebaseToken.getIssuer()); + assertEquals("test@example.com", firebaseToken.getEmail()); + assertTrue(firebaseToken.isEmailVerified()); + assertEquals("Test User", firebaseToken.getName()); + assertEquals("https://picture.url", firebaseToken.getPicture()); + assertEquals("claim", firebaseToken.getClaims().get("custom")); + assertEquals(7, firebaseToken.getClaims().size()); + } + + @Test + public void testFirebaseTokenFromMinimalIdToken() { + IdToken.Payload payload = new IdToken.Payload() + .setSubject("testUser"); + + FirebaseToken firebaseToken = new FirebaseToken(payload); + + assertEquals("testUser", firebaseToken.getUid()); + assertNull(firebaseToken.getIssuer()); + assertNull(firebaseToken.getEmail()); + assertFalse(firebaseToken.isEmailVerified()); + assertNull(firebaseToken.getName()); + assertNull(firebaseToken.getPicture()); + assertEquals(1, firebaseToken.getClaims().size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testFirebaseTokenNullClaims() { + new FirebaseToken(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testFirebaseTokenNoUid() { + ImmutableMap claimsWithoutSub = ImmutableMap.of( + "custom", "claim"); + new FirebaseToken(claimsWithoutSub); + } +} diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenUtilsTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenUtilsTest.java new file mode 100644 index 000000000..f452091fc --- /dev/null +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenUtilsTest.java @@ -0,0 +1,130 @@ +/* + * Copyright 2019 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 junit.framework.TestCase.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.auth.openidconnect.IdTokenVerifier; +import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.testing.http.FixedClock; +import com.google.api.client.util.Clock; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.collect.Iterables; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class FirebaseTokenUtilsTest { + + private static final Clock CLOCK = new FixedClock(2002000L * 1000); + private static final String TEST_PROJECT_ID = "test-project-id"; + private static final GoogleCredentials MOCK_CREDENTIALS = GoogleCredentials.create(null); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testCreateIdTokenVerifier() { + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setProjectId(TEST_PROJECT_ID) + .build()); + + FirebaseTokenVerifierImpl idTokenVerifier = FirebaseTokenUtils.createIdTokenVerifier( + app, CLOCK); + + assertEquals("verifyIdToken()", idTokenVerifier.getMethod()); + assertEquals("ID token", idTokenVerifier.getShortName()); + assertEquals("an ID token", idTokenVerifier.getArticledShortName()); + assertEquals("https://firebase.google.com/docs/auth/admin/verify-id-tokens", + idTokenVerifier.getDocUrl()); + verifyPublicKeysManager(idTokenVerifier.getPublicKeysManager(), + "https://www.googleapis.com/robot/v1/metadata/x509/" + + "securetoken@system.gserviceaccount.com"); + verifyJwtVerifier(idTokenVerifier.getIdTokenVerifier(), + "https://securetoken.google.com/test-project-id"); + } + + @Test + public void testCreateIdTokenVerifierWithoutProjectId() { + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .build()); + + thrown.expectMessage("Must initialize FirebaseApp with a project ID to call verifyIdToken()"); + FirebaseTokenUtils.createIdTokenVerifier(app, CLOCK); + } + + @Test + public void testSessionCookieVerifier() { + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .setProjectId(TEST_PROJECT_ID) + .build()); + + FirebaseTokenVerifierImpl cookieVerifier = FirebaseTokenUtils.createSessionCookieVerifier( + app, CLOCK); + + assertEquals("verifySessionCookie()", cookieVerifier.getMethod()); + assertEquals("session cookie", cookieVerifier.getShortName()); + assertEquals("a session cookie", cookieVerifier.getArticledShortName()); + assertEquals("https://firebase.google.com/docs/auth/admin/manage-cookies", + cookieVerifier.getDocUrl()); + verifyPublicKeysManager(cookieVerifier.getPublicKeysManager(), + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys"); + verifyJwtVerifier(cookieVerifier.getIdTokenVerifier(), + "https://session.firebase.google.com/test-project-id"); + } + + @Test + public void testCreateSessionCookieVerifierWithoutProjectId() { + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(MOCK_CREDENTIALS) + .build()); + + thrown.expectMessage("Must initialize FirebaseApp with a project ID to call " + + "verifySessionCookie()"); + FirebaseTokenUtils.createSessionCookieVerifier(app, CLOCK); + } + + private void verifyPublicKeysManager(GooglePublicKeysManager publicKeysManager, String certUrl) { + assertNotNull(publicKeysManager); + assertEquals(certUrl, publicKeysManager.getPublicCertsEncodedUrl()); + assertSame(CLOCK, publicKeysManager.getClock()); + assertTrue(publicKeysManager.getJsonFactory() instanceof GsonFactory); + } + + private void verifyJwtVerifier(IdTokenVerifier jwtVerifier, String issuer) { + assertNotNull(jwtVerifier); + assertEquals(issuer, jwtVerifier.getIssuer()); + assertEquals(TEST_PROJECT_ID, Iterables.getOnlyElement(jwtVerifier.getAudience())); + assertSame(CLOCK, jwtVerifier.getClock()); + } +} diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java new file mode 100644 index 000000000..b10817afb --- /dev/null +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -0,0 +1,340 @@ +/* + * Copyright 2019 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.assertTrue; + +import com.google.api.client.auth.openidconnect.IdTokenVerifier; +import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.json.webtoken.JsonWebSignature; +import com.google.api.client.json.webtoken.JsonWebToken.Payload; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.firebase.testing.ServiceAccount; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class FirebaseTokenVerifierImplTest { + + private static final String CUSTOM_TOKEN_AUDIENCE = + "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit" + + ".v1.IdentityToolkit"; + + private static final String LEGACY_CUSTOM_TOKEN = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkIjp7" + + "InVpZCI6IjEiLCJhYmMiOiIwMTIzNDU2Nzg5fiFAIyQlXiYqKClfKy09YWJjZGVmZ2hpamtsbW5vcHF" + + "yc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWiwuLzsnW11cXDw" + + "-P1wie318In0sInYiOjAsImlhdCI6MTQ4MDk4Mj" + + "U2NH0.ZWEpoHgIPCAz8Q-cNFBS8jiqClTJ3j27yuRkQo-QxyI"; + + private static final String TEST_TOKEN_ISSUER = "https://test.token.issuer"; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private FirebaseTokenVerifier tokenVerifier; + private TestTokenFactory tokenFactory; + + @Before + public void setUp() throws Exception { + ServiceAccount serviceAccount = ServiceAccount.EDITOR; + GooglePublicKeysManager publicKeysManager = newPublicKeysManager(serviceAccount.getCert()); + this.tokenVerifier = newTestTokenVerifier(publicKeysManager); + this.tokenFactory = new TestTokenFactory(serviceAccount.getPrivateKey(), TEST_TOKEN_ISSUER); + } + + @Test + public void testVerifyToken() throws Exception { + String token = tokenFactory.createToken(); + + FirebaseToken firebaseToken = tokenVerifier.verifyToken(token); + + assertEquals(TEST_TOKEN_ISSUER, firebaseToken.getIssuer()); + assertEquals(TestTokenFactory.UID, firebaseToken.getUid()); + } + + @Test + public void testVerifyTokenWithoutKeyId() throws Exception { + String token = createTokenWithoutKeyId(); + + thrown.expectMessage("Firebase test token has no \"kid\" claim."); + tokenVerifier.verifyToken(token); + } + + @Test + public void testVerifyTokenFirebaseCustomToken() throws Exception { + String token = createCustomToken(); + + thrown.expectMessage("verifyTestToken() expects a test token, but was given a custom token."); + tokenVerifier.verifyToken(token); + } + + @Test + public void testVerifyTokenIncorrectAlgorithm() throws Exception { + String token = createTokenWithIncorrectAlgorithm(); + + thrown.expectMessage("Firebase test token has incorrect algorithm."); + tokenVerifier.verifyToken(token); + } + + @Test + public void testVerifyTokenIncorrectAudience() throws Exception { + String token = createTokenWithIncorrectAudience(); + + thrown.expectMessage("Firebase test token has incorrect \"aud\" (audience) claim."); + tokenVerifier.verifyToken(token); + } + + @Test + public void testVerifyTokenIncorrectIssuer() throws Exception { + String token = createTokenWithIncorrectIssuer(); + + thrown.expectMessage("Firebase test token has incorrect \"iss\" (issuer) claim."); + tokenVerifier.verifyToken(token); + } + + @Test + public void testVerifyTokenMissingSubject() throws Exception { + String token = createTokenWithSubject(null); + + thrown.expectMessage("Firebase test token has no \"sub\" (subject) claim."); + tokenVerifier.verifyToken(token); + } + + @Test + public void testVerifyTokenEmptySubject() throws Exception { + String token = createTokenWithSubject(""); + + thrown.expectMessage("Firebase test token has an empty string \"sub\" (subject) claim."); + tokenVerifier.verifyToken(token); + } + + @Test + public void testVerifyTokenLongSubject() throws Exception { + String token = createTokenWithSubject(Strings.repeat("a", 129)); + + thrown.expectMessage( + "Firebase test token has \"sub\" (subject) claim longer than 128 characters."); + tokenVerifier.verifyToken(token); + } + + @Test + public void testVerifyTokenIssuedAtInFuture() throws Exception { + long tenMinutesIntoTheFuture = (TestTokenFactory.CLOCK.currentTimeMillis() / 1000) + + TimeUnit.MINUTES.toSeconds(10); + String token = createTokenWithTimestamps( + tenMinutesIntoTheFuture, + tenMinutesIntoTheFuture + TimeUnit.HOURS.toSeconds(1)); + + thrown.expectMessage("Firebase test token has expired or is not yet valid."); + tokenVerifier.verifyToken(token); + } + + @Test + public void testVerifyTokenExpired() throws Exception { + long twoHoursInPast = (TestTokenFactory.CLOCK.currentTimeMillis() / 1000) + - TimeUnit.HOURS.toSeconds(2); + String token = createTokenWithTimestamps( + twoHoursInPast, + twoHoursInPast + TimeUnit.HOURS.toSeconds(1)); + + thrown.expectMessage("Firebase test token has expired or is not yet valid."); + tokenVerifier.verifyToken(token); + } + + @Test + public void testVerifyTokenIncorrectCert() throws Exception { + String token = tokenFactory.createToken(); + GooglePublicKeysManager publicKeysManager = newPublicKeysManager( + ServiceAccount.NONE.getCert()); + FirebaseTokenVerifier tokenVerifier = newTestTokenVerifier(publicKeysManager); + + thrown.expectMessage("Failed to verify the signature of Firebase test token. " + + "See https://test.doc.url for details on how to retrieve a test token."); + tokenVerifier.verifyToken(token); + } + + @Test + public void verifyTokenCertificateError() { + MockHttpTransport failingTransport = new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + throw new IOException("Expected error"); + } + }; + GooglePublicKeysManager publicKeysManager = newPublicKeysManager(failingTransport); + FirebaseTokenVerifier idTokenVerifier = newTestTokenVerifier(publicKeysManager); + String token = tokenFactory.createToken(); + + try { + idTokenVerifier.verifyToken(token); + Assert.fail("No exception thrown"); + } catch (FirebaseAuthException expected) { + assertTrue(expected.getCause() instanceof IOException); + assertEquals("Expected error", expected.getCause().getMessage()); + } + } + + @Test + public void testLegacyCustomToken() throws Exception { + thrown.expectMessage( + "verifyTestToken() expects a test token, but was given a legacy custom token."); + tokenVerifier.verifyToken(LEGACY_CUSTOM_TOKEN); + } + + @Test + public void testMalformedToken() throws Exception { + thrown.expectMessage( + "Failed to parse Firebase test token. Make sure you passed a string that represents a " + + "complete and valid JWT. See https://test.doc.url for details on how to retrieve " + + "a test token."); + tokenVerifier.verifyToken("not.a.jwt"); + } + + @Test(expected = NullPointerException.class) + public void testBuilderNoPublicKeysManager() { + fullyPopulatedBuilder().setPublicKeysManager(null).build(); + } + + @Test(expected = NullPointerException.class) + public void testBuilderNoJsonFactory() { + fullyPopulatedBuilder().setJsonFactory(null).build(); + } + + @Test(expected = NullPointerException.class) + public void testBuilderNoIdTokenVerifier() { + fullyPopulatedBuilder().setIdTokenVerifier(null).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBuilderNoMethodName() { + fullyPopulatedBuilder().setMethod(null).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBuilderNoShortName() { + fullyPopulatedBuilder().setShortName(null).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBuilderNoDocUrl() { + fullyPopulatedBuilder().setDocUrl(null).build(); + } + + private GooglePublicKeysManager newPublicKeysManager(String certificate) { + String serviceAccountCertificates = + String.format("{\"%s\" : \"%s\"}", TestTokenFactory.PRIVATE_KEY_ID, certificate); + HttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse( + new MockLowLevelHttpResponse().setContent(serviceAccountCertificates)) + .build(); + return newPublicKeysManager(transport); + } + + private GooglePublicKeysManager newPublicKeysManager(HttpTransport transport) { + return new GooglePublicKeysManager.Builder( + transport, FirebaseTokenUtils.UNQUOTED_CTRL_CHAR_JSON_FACTORY) + .setClock(TestTokenFactory.CLOCK) + .setPublicCertsEncodedUrl("https://test.cert.url") + .build(); + } + + private FirebaseTokenVerifier newTestTokenVerifier(GooglePublicKeysManager publicKeysManager) { + return FirebaseTokenVerifierImpl.builder() + .setShortName("test token") + .setMethod("verifyTestToken()") + .setDocUrl("https://test.doc.url") + .setJsonFactory(TestTokenFactory.JSON_FACTORY) + .setPublicKeysManager(publicKeysManager) + .setIdTokenVerifier(newIdTokenVerifier()) + .build(); + } + + private FirebaseTokenVerifierImpl.Builder fullyPopulatedBuilder() { + return FirebaseTokenVerifierImpl.builder() + .setShortName("test token") + .setMethod("verifyTestToken()") + .setDocUrl("https://test.doc.url") + .setJsonFactory(TestTokenFactory.JSON_FACTORY) + .setPublicKeysManager(newPublicKeysManager(ServiceAccount.EDITOR.getCert())) + .setIdTokenVerifier(newIdTokenVerifier()); + } + + private IdTokenVerifier newIdTokenVerifier() { + return new IdTokenVerifier.Builder() + .setClock(TestTokenFactory.CLOCK) + .setAudience(ImmutableList.of(TestTokenFactory.PROJECT_ID)) + .setIssuer(TEST_TOKEN_ISSUER) + .build(); + } + + private String createTokenWithoutKeyId() { + JsonWebSignature.Header header = tokenFactory.createHeader(); + header.setKeyId(null); + return tokenFactory.createToken(header); + } + + private String createTokenWithSubject(String sub) { + Payload payload = tokenFactory.createTokenPayload(); + payload.setSubject(sub); + return tokenFactory.createToken(payload); + } + + private String createCustomToken() { + JsonWebSignature.Header header = tokenFactory.createHeader(); + header.setKeyId(null); + Payload payload = tokenFactory.createTokenPayload(); + payload.setAudience(CUSTOM_TOKEN_AUDIENCE); + return tokenFactory.createToken(header, payload); + } + + private String createTokenWithIncorrectAlgorithm() { + JsonWebSignature.Header header = tokenFactory.createHeader(); + header.setAlgorithm("HSA"); + return tokenFactory.createToken(header); + } + + private String createTokenWithIncorrectAudience() { + Payload payload = tokenFactory.createTokenPayload(); + payload.setAudience("invalid-audience"); + return tokenFactory.createToken(payload); + } + + private String createTokenWithIncorrectIssuer() { + Payload payload = tokenFactory.createTokenPayload(); + payload.setIssuer("https://incorrect.issuer.prefix/" + TestTokenFactory.PROJECT_ID); + return tokenFactory.createToken(payload); + } + + private String createTokenWithTimestamps(long issuedAtSeconds, long expirationSeconds) { + Payload payload = tokenFactory.createTokenPayload(); + payload.setIssuedAtTimeSeconds(issuedAtSeconds); + payload.setExpirationTimeSeconds(expirationSeconds); + return tokenFactory.createToken(payload); + } +} diff --git a/src/test/java/com/google/firebase/auth/TestOnlyImplFirebaseAuthTrampolines.java b/src/test/java/com/google/firebase/auth/TestOnlyImplFirebaseAuthTrampolines.java deleted file mode 100644 index fb66500bf..000000000 --- a/src/test/java/com/google/firebase/auth/TestOnlyImplFirebaseAuthTrampolines.java +++ /dev/null @@ -1,43 +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.auth; - -import com.google.api.client.json.JsonFactory; - -import java.io.IOException; - -/** - * Provides trampolines into package-private Auth APIs used by components of Firebase - * - *

      This class will not be compiled into the shipping library and can only be used in tests. - */ -public final class TestOnlyImplFirebaseAuthTrampolines { - - private TestOnlyImplFirebaseAuthTrampolines() {} - - /* FirebaseApp */ - public static FirebaseToken.FirebaseTokenImpl getToken(FirebaseToken tokenHolder) { - return tokenHolder.getToken(); - } - - /* FirebaseToken */ - public static FirebaseToken parseToken(JsonFactory jsonFactory, String tokenString) - throws IOException { - return FirebaseToken.parse(jsonFactory, tokenString); - } - -} diff --git a/src/test/java/com/google/firebase/auth/TestTokenFactory.java b/src/test/java/com/google/firebase/auth/TestTokenFactory.java new file mode 100644 index 000000000..1f774ee7c --- /dev/null +++ b/src/test/java/com/google/firebase/auth/TestTokenFactory.java @@ -0,0 +1,89 @@ +/* + * Copyright 2019 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 com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.webtoken.JsonWebSignature; +import com.google.api.client.json.webtoken.JsonWebToken; +import com.google.api.client.testing.http.FixedClock; +import com.google.api.client.util.Clock; +import com.google.common.io.BaseEncoding; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; + +class TestTokenFactory { + + public static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + public static final Clock CLOCK = new FixedClock(2002000L * 1000); + public static final String PROJECT_ID = "proj-test-101"; + public static final String PRIVATE_KEY_ID = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd"; + public static final String UID = "someUid"; + + + private final PrivateKey privateKey; + private final String issuer; + + TestTokenFactory(String privateKey, String issuer) throws GeneralSecurityException { + byte[] privateBytes = BaseEncoding.base64().decode(privateKey); + KeySpec spec = new PKCS8EncodedKeySpec(privateBytes); + this.privateKey = KeyFactory.getInstance("RSA").generatePrivate(spec); + this.issuer = issuer; + } + + public String createToken() { + return createToken(createTokenPayload()); + } + + public String createToken(JsonWebSignature.Header header) { + return createToken(header, createTokenPayload()); + } + + public String createToken(JsonWebSignature.Payload payload) { + return createToken(createHeader(), payload); + } + + public String createToken(JsonWebSignature.Header header, JsonWebToken.Payload payload) { + try { + return JsonWebSignature.signUsingRsaSha256(privateKey, JSON_FACTORY, header, payload); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Failed to create test token", e); + } + } + + public JsonWebSignature.Header createHeader() { + JsonWebSignature.Header header = new JsonWebSignature.Header(); + header.setAlgorithm("RS256"); + header.setType("JWT"); + header.setKeyId(PRIVATE_KEY_ID); + return header; + } + + public JsonWebToken.Payload createTokenPayload() { + JsonWebToken.Payload payload = new JsonWebToken.Payload(); + payload.setIssuer(issuer); + payload.setAudience(PROJECT_ID); + payload.setIssuedAtTimeSeconds(CLOCK.currentTimeMillis() / 1000); + payload.setExpirationTimeSeconds(CLOCK.currentTimeMillis() / 1000 + 3600); + payload.setSubject(UID); + return payload; + } +} diff --git a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java deleted file mode 100644 index da92a75b8..000000000 --- a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenVerifierTest.java +++ /dev/null @@ -1,476 +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.auth.internal; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import com.google.api.client.auth.openidconnect.IdToken; -import com.google.api.client.http.LowLevelHttpRequest; -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.json.webtoken.JsonWebSignature.Header; -import com.google.api.client.json.webtoken.JsonWebToken; -import com.google.api.client.json.webtoken.JsonWebToken.Payload; -import com.google.api.client.testing.http.FixedClock; -import com.google.api.client.testing.http.MockHttpTransport; -import com.google.api.client.testing.http.MockLowLevelHttpResponse; -import com.google.common.io.BaseEncoding; -import com.google.firebase.auth.FirebaseAuthException; -import com.google.firebase.auth.FirebaseToken; -import com.google.firebase.auth.TestOnlyImplFirebaseAuthTrampolines; -import com.google.firebase.testing.ServiceAccount; -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.KeySpec; -import java.security.spec.PKCS8EncodedKeySpec; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -/** - * Unit tests for {@link FirebaseTokenVerifier}. - */ -public class FirebaseTokenVerifierTest { - - private static final JsonFactory FACTORY = new GsonFactory(); - private static final FixedClock CLOCK = new FixedClock(2002000L * 1000); - private static final String PROJECT_ID = "proj-test-101"; - private static final String TOKEN_ISSUER = - FirebaseTokenVerifier.ID_TOKEN_ISSUER_PREFIX + PROJECT_ID; - private static final String COOKIE_ISSUER = - FirebaseTokenVerifier.SESSION_COOKIE_ISSUER_PREFIX + PROJECT_ID; - private static final String PRIVATE_KEY_ID = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd"; - private static final String UID = "someUid"; - private static final String ALGORITHM = "RS256"; - private static final String LEGACY_CUSTOM_TOKEN = - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkIjp7" - + "InVpZCI6IjEiLCJhYmMiOiIwMTIzNDU2Nzg5fiFAIyQlXiYqKClfKy09YWJjZGVmZ2hpamtsbW5vcHF" - + "yc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWiwuLzsnW11cXDw" - + "-P1wie318In0sInYiOjAsImlhdCI6MTQ4MDk4Mj" - + "U2NH0.ZWEpoHgIPCAz8Q-cNFBS8jiqClTJ3j27yuRkQo-QxyI"; - - @Rule public ExpectedException thrown = ExpectedException.none(); - - private PrivateKey privateKey; - private FirebaseTokenVerifier idTokenVerifier; - private FirebaseTokenVerifier cookieVerifier; - - private void initCrypto(String privateKey, String certificate) - throws NoSuchAlgorithmException, InvalidKeySpecException { - byte[] privateBytes = BaseEncoding.base64().decode(privateKey); - KeySpec spec = new PKCS8EncodedKeySpec(privateBytes); - String serviceAccountCertificates = - String.format("{\"%s\" : \"%s\"}", PRIVATE_KEY_ID, certificate); - - MockHttpTransport mockTransport = - new MockHttpTransport.Builder() - .setLowLevelHttpResponse( - new MockLowLevelHttpResponse().setContent(serviceAccountCertificates)) - .build(); - KeyManagers keyManagers = KeyManagers.getDefault(mockTransport, CLOCK); - this.privateKey = KeyFactory.getInstance("RSA").generatePrivate(spec); - this.idTokenVerifier = FirebaseTokenVerifier.createIdTokenVerifier( - PROJECT_ID, keyManagers, CLOCK); - this.cookieVerifier = FirebaseTokenVerifier.createSessionCookieVerifier( - PROJECT_ID, keyManagers, CLOCK); - } - - @Before - public void setUp() throws Exception { - initCrypto(ServiceAccount.EDITOR.getPrivateKey(), ServiceAccount.EDITOR.getCert()); - } - - private JsonWebSignature.Header createHeader() { - JsonWebSignature.Header header = new JsonWebSignature.Header(); - header.setAlgorithm(ALGORITHM); - header.setType("JWT"); - header.setKeyId(PRIVATE_KEY_ID); - return header; - } - - private JsonWebToken.Payload createTokenPayload() { - JsonWebToken.Payload payload = new JsonWebToken.Payload(); - payload.setIssuer(TOKEN_ISSUER); - payload.setAudience(PROJECT_ID); - payload.setIssuedAtTimeSeconds(CLOCK.currentTimeMillis() / 1000); - payload.setExpirationTimeSeconds(CLOCK.currentTimeMillis() / 1000 + 3600); - payload.setSubject(UID); - return payload; - } - - private JsonWebToken.Payload createCookiePayload() { - JsonWebToken.Payload payload = new JsonWebToken.Payload(); - payload.setIssuer(COOKIE_ISSUER); - payload.setAudience(PROJECT_ID); - payload.setIssuedAtTimeSeconds(CLOCK.currentTimeMillis() / 1000); - payload.setExpirationTimeSeconds(CLOCK.currentTimeMillis() / 1000 + 3600); - payload.setSubject(UID); - return payload; - } - - private String createToken(JsonWebSignature.Header header, JsonWebToken.Payload payload) - throws GeneralSecurityException, IOException { - return JsonWebSignature.signUsingRsaSha256(privateKey, FACTORY, header, payload); - } - - @Test - public void verifyToken() throws Exception { - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), createTokenPayload())); - - IdToken.Payload payload = (IdToken.Payload) token.getClaims(); - assertTrue(payload.getAudienceAsList().contains(PROJECT_ID)); - assertEquals(TOKEN_ISSUER, payload.getIssuer()); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - - token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), createCookiePayload())); - thrown.expectMessage("Firebase ID token has incorrect \"iss\" (issuer) claim."); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyCookie() throws Exception { - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), createCookiePayload())); - - IdToken.Payload payload = (IdToken.Payload) token.getClaims(); - assertTrue(payload.getAudienceAsList().contains(PROJECT_ID)); - assertEquals(COOKIE_ISSUER, payload.getIssuer()); - cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - - token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), createTokenPayload())); - thrown.expectMessage("Firebase session cookie has incorrect \"iss\" (issuer) claim."); - cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyTokenFailure_MissingKeyId() throws Exception { - Header header = createHeader(); - header.setKeyId(null); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(header, createTokenPayload())); - thrown.expectMessage("Firebase ID token has no \"kid\" claim."); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyCookieFailure_MissingKeyId() throws Exception { - Header header = createHeader(); - header.setKeyId(null); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(header, createCookiePayload())); - thrown.expectMessage("Firebase session cookie has no \"kid\" claim."); - cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyTokenFailure_MissingKeyId_CustomToken() throws Exception { - Header header = createHeader(); - header.setKeyId(null); - Payload payload = createTokenPayload(); - payload.setAudience( - "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit" - + ".v1.IdentityToolkit"); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken(FACTORY, createToken(header, payload)); - thrown.expectMessage("verifyIdToken() expects an ID token, but was given a custom token."); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyCookieFailure_MissingKeyId_CustomToken() throws Exception { - Header header = createHeader(); - header.setKeyId(null); - Payload payload = createCookiePayload(); - payload.setAudience( - "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit" - + ".v1.IdentityToolkit"); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken(FACTORY, createToken(header, payload)); - thrown.expectMessage( - "verifySessionCookie() expects a session cookie, but was given a custom token."); - cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyTokenFailure_IncorrectAlgorithm() throws Exception { - Header header = createHeader(); - header.setAlgorithm("HS256"); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(header, createTokenPayload())); - thrown.expectMessage("Firebase ID token has incorrect algorithm."); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyCookieFailure_IncorrectAlgorithm() throws Exception { - Header header = createHeader(); - header.setAlgorithm("HS256"); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(header, createCookiePayload())); - thrown.expectMessage("Firebase session cookie has incorrect algorithm."); - cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyTokenFailure_IncorrectAudience() throws Exception { - Payload payload = createTokenPayload(); - payload.setAudience( - "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1." - + "IdentityToolkit"); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage("Firebase ID token has incorrect \"aud\" (audience) claim."); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyCookieFailure_IncorrectAudience() throws Exception { - Payload payload = createCookiePayload(); - payload.setAudience( - "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1." - + "IdentityToolkit"); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage("Firebase session cookie has incorrect \"aud\" (audience) claim."); - cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyTokenFailure_IncorrectIssuer() throws Exception { - Payload payload = createTokenPayload(); - payload.setIssuer("https://foobar.google.com/" + PROJECT_ID); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage("Firebase ID token has incorrect \"iss\" (issuer) claim."); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyCookieFailure_IncorrectIssuer() throws Exception { - Payload payload = createCookiePayload(); - payload.setIssuer("https://foobar.google.com/" + PROJECT_ID); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage("Firebase session cookie has incorrect \"iss\" (issuer) claim."); - cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyTokenFailure_MissingSubject() throws Exception { - Payload payload = createTokenPayload(); - payload.setSubject(null); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage("Firebase ID token has no \"sub\" (subject) claim."); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyCookieFailure_MissingSubject() throws Exception { - Payload payload = createCookiePayload(); - payload.setSubject(null); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage("Firebase session cookie has no \"sub\" (subject) claim."); - cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyTokenFailure_EmptySubject() throws Exception { - Payload payload = createTokenPayload(); - payload.setSubject(""); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage("Firebase ID token has an empty string \"sub\" (subject) claim."); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyCookieFailure_EmptySubject() throws Exception { - Payload payload = createCookiePayload(); - payload.setSubject(""); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage("Firebase session cookie has an empty string \"sub\" (subject) claim."); - cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyTokenFailure_LongSubject() throws Exception { - Payload payload = createTokenPayload(); - payload.setSubject( - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv" - + "wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage( - "Firebase ID token has \"sub\" (subject) claim longer than 128 characters."); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyCookieFailure_LongSubject() throws Exception { - Payload payload = createCookiePayload(); - payload.setSubject( - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv" - + "wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage( - "Firebase session cookie has \"sub\" (subject) claim longer than 128 characters."); - cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyTokenFailure_NotYetIssued() throws Exception { - Payload payload = createTokenPayload(); - payload.setIssuedAtTimeSeconds(System.currentTimeMillis() / 1000); - payload.setExpirationTimeSeconds(System.currentTimeMillis() / 1000 + 3600); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage("Firebase ID token has expired or is not yet valid."); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyCookieFailure_NotYetIssued() throws Exception { - Payload payload = createCookiePayload(); - payload.setIssuedAtTimeSeconds(System.currentTimeMillis() / 1000); - payload.setExpirationTimeSeconds(System.currentTimeMillis() / 1000 + 3600); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage("Firebase session cookie has expired or is not yet valid."); - cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyTokenFailure_Expired() throws Exception { - Payload payload = createTokenPayload(); - payload.setIssuedAtTimeSeconds(0L); - payload.setExpirationTimeSeconds(3600L); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage("Firebase ID token has expired or is not yet valid."); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyCookieFailure_Expired() throws Exception { - Payload payload = createCookiePayload(); - payload.setIssuedAtTimeSeconds(0L); - payload.setExpirationTimeSeconds(3600L); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), payload)); - thrown.expectMessage("Firebase session cookie has expired or is not yet valid."); - cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyTokenFailure_WrongCert() throws Exception { - initCrypto(ServiceAccount.OWNER.getPrivateKey(), ServiceAccount.NONE.getCert()); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), createTokenPayload())); - thrown.expectMessage("Firebase ID token isn't signed by a valid public key."); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - - token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), createCookiePayload())); - thrown.expectMessage("Firebase session cookie isn't signed by a valid public key."); - cookieVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } - - @Test - public void verifyTokenCertificateError() throws Exception { - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), createTokenPayload())); - - MockHttpTransport mockTransport = new MockHttpTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - throw new IOException("Expected error"); - } - }; - KeyManagers keyManagers = KeyManagers.getDefault(mockTransport, CLOCK); - FirebaseTokenVerifier verifier = FirebaseTokenVerifier.createIdTokenVerifier( - PROJECT_ID, keyManagers, CLOCK); - try { - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - Assert.fail("No exception thrown"); - } catch (FirebaseAuthException expected) { - assertTrue(expected.getCause() instanceof IOException); - assertEquals("Expected error", expected.getCause().getMessage()); - } - - token = - TestOnlyImplFirebaseAuthTrampolines.parseToken( - FACTORY, createToken(createHeader(), createCookiePayload())); - verifier = FirebaseTokenVerifier.createSessionCookieVerifier(PROJECT_ID, keyManagers, CLOCK); - try { - verifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - Assert.fail("No exception thrown"); - } catch (FirebaseAuthException expected) { - assertTrue(expected.getCause() instanceof IOException); - assertEquals("Expected error", expected.getCause().getMessage()); - } - } - - @Test - public void legacyCustomToken() throws Exception { - initCrypto(ServiceAccount.OWNER.getPrivateKey(), ServiceAccount.NONE.getCert()); - FirebaseToken token = - TestOnlyImplFirebaseAuthTrampolines.parseToken(FACTORY, LEGACY_CUSTOM_TOKEN); - thrown.expectMessage( - "verifyIdToken() expects an ID token, but was given a legacy custom token."); - idTokenVerifier.verifyTokenAndSignature(TestOnlyImplFirebaseAuthTrampolines.getToken(token)); - } -} From f2236e058cefc644cd63a7de1dfd5eff88b296aa Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 14 Mar 2019 15:34:52 -0700 Subject: [PATCH 057/456] HTTP Retry Support (#255) * Basic framework for HTTP retries * Implementing support for retry-after header * Cleaned up the retry-after processing logic * Moved the status code checking logic * Updated tests * Updated class names and tests * Refactored retry impl and tests * Simplified the retry handler * More tests and docs * Further cleaned up the impl and tests * Decoupled retry initializer from credentials * More code cleanup * Cleaning up tests * Not calling any retry code when RetryConfig = null * Added an option to enable/disable retries on IO errors. Added some comments * New test case * Updated some comments; Cleaned up tests * Fixed a typo in a comment * Removing the hard dependency on Apache HTTP Client (#259) * Copied DateUtils source from Apache HC * Updated reference link * Used locks instead of thread locals; Added tests * Added a NOTICE file for third-party code --- NOTICE.txt | 5 + .../google/firebase/internal/DateUtils.java | 108 ++++++++ .../internal/FirebaseRequestInitializer.java | 55 +++- .../google/firebase/internal/RetryConfig.java | 177 ++++++++++++ .../firebase/internal/RetryInitializer.java | 112 ++++++++ .../RetryUnsuccessfulResponseHandler.java | 112 ++++++++ .../internal/CountingLowLevelHttpRequest.java | 74 +++++ .../firebase/internal/DateUtilsTest.java | 80 ++++++ .../FirebaseRequestInitializerTest.java | 119 ++++++-- .../firebase/internal/RetryConfigTest.java | 126 +++++++++ .../internal/RetryInitializerTest.java | 244 +++++++++++++++++ .../RetryUnsuccessfulResponseHandlerTest.java | 258 ++++++++++++++++++ .../google/firebase/testing/TestUtils.java | 21 +- 13 files changed, 1456 insertions(+), 35 deletions(-) create mode 100644 NOTICE.txt create mode 100644 src/main/java/com/google/firebase/internal/DateUtils.java create mode 100644 src/main/java/com/google/firebase/internal/RetryConfig.java create mode 100644 src/main/java/com/google/firebase/internal/RetryInitializer.java create mode 100644 src/main/java/com/google/firebase/internal/RetryUnsuccessfulResponseHandler.java create mode 100644 src/test/java/com/google/firebase/internal/CountingLowLevelHttpRequest.java create mode 100644 src/test/java/com/google/firebase/internal/DateUtilsTest.java create mode 100644 src/test/java/com/google/firebase/internal/RetryConfigTest.java create mode 100644 src/test/java/com/google/firebase/internal/RetryInitializerTest.java create mode 100644 src/test/java/com/google/firebase/internal/RetryUnsuccessfulResponseHandlerTest.java diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 000000000..b6c5d02b2 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,5 @@ +Firebase Admin Java SDK +Copyright 2019 Google Inc. + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/src/main/java/com/google/firebase/internal/DateUtils.java b/src/main/java/com/google/firebase/internal/DateUtils.java new file mode 100644 index 000000000..6c4eeb7b0 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/DateUtils.java @@ -0,0 +1,108 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +/** + * A utility class for parsing and formatting HTTP dates as used in cookies and + * other headers. This class handles dates as defined by RFC 2616 section + * 3.3.1 as well as some other common non-standard formats. + * + *

      Most of this class was borrowed from the + * + * Apache HTTP client in order to avoid a direct dependency on it. We currently + * have a transitive dependency on this library (via Google API client), but the API + * client team is working towards removing it, so we won't have it in the classpath for long. + * + *

      The original implementation of this class uses + * thread locals to cache the {@code SimpleDateFormat} instances. Instead, this implementation + * uses static constants and explicit locking to ensure thread safety. This is probably slower, + * but also simpler and avoids memory leaks that may result from unreleased thread locals. + */ +final class DateUtils { + + /** + * Date format pattern used to parse HTTP date headers in RFC 1123 format. + */ + static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz"; + + /** + * Date format pattern used to parse HTTP date headers in RFC 1036 format. + */ + static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz"; + + /** + * Date format pattern used to parse HTTP date headers in ANSI C + * {@code asctime()} format. + */ + static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy"; + + private static final SimpleDateFormat[] DEFAULT_PATTERNS = new SimpleDateFormat[] { + new SimpleDateFormat(PATTERN_RFC1123), + new SimpleDateFormat(PATTERN_RFC1036), + new SimpleDateFormat(PATTERN_ASCTIME) + }; + + static final TimeZone GMT = TimeZone.getTimeZone("GMT"); + + static { + final Calendar calendar = Calendar.getInstance(); + calendar.setTimeZone(GMT); + calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0); + calendar.set(Calendar.MILLISECOND, 0); + final Date defaultTwoDigitYearStart = calendar.getTime(); + + for (final SimpleDateFormat datePattern : DEFAULT_PATTERNS) { + datePattern.set2DigitYearStart(defaultTwoDigitYearStart); + } + } + + /** + * Parses the date value using the given date formats. + * + * @param dateValue the date value to parse + * @return the parsed date or null if input could not be parsed + */ + public static Date parseDate(final String dateValue) { + String v = checkNotNull(dateValue); + // trim single quotes around date if present + // see issue #5279 + if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) { + v = v.substring(1, v.length() - 1); + } + + for (final SimpleDateFormat datePattern : DEFAULT_PATTERNS) { + final ParsePosition pos = new ParsePosition(0); + synchronized (datePattern) { + final Date result = datePattern.parse(v, pos); + if (pos.getIndex() != 0) { + return result; + } + } + } + return null; + } + + /** This class should not be instantiated. */ + private DateUtils() { + } +} diff --git a/src/main/java/com/google/firebase/internal/FirebaseRequestInitializer.java b/src/main/java/com/google/firebase/internal/FirebaseRequestInitializer.java index 0cd4e9393..e73f93953 100644 --- a/src/main/java/com/google/firebase/internal/FirebaseRequestInitializer.java +++ b/src/main/java/com/google/firebase/internal/FirebaseRequestInitializer.java @@ -19,32 +19,57 @@ import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestInitializer; import com.google.auth.http.HttpCredentialsAdapter; -import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; import com.google.firebase.ImplFirebaseTrampolines; import java.io.IOException; +import java.util.List; /** - * {@code HttpRequestInitializer} for configuring outgoing REST calls. Handles OAuth2 authorization - * and setting timeout values. + * {@code HttpRequestInitializer} for configuring outgoing REST calls. Initializes requests with + * OAuth2 credentials, timeout and retry settings. */ -public class FirebaseRequestInitializer implements HttpRequestInitializer { +public final class FirebaseRequestInitializer implements HttpRequestInitializer { - private final HttpCredentialsAdapter credentialsAdapter; - private final int connectTimeout; - private final int readTimeout; + private final List initializers; public FirebaseRequestInitializer(FirebaseApp app) { - GoogleCredentials credentials = ImplFirebaseTrampolines.getCredentials(app); - this.credentialsAdapter = new HttpCredentialsAdapter(credentials); - this.connectTimeout = app.getOptions().getConnectTimeout(); - this.readTimeout = app.getOptions().getReadTimeout(); + this(app, null); + } + + public FirebaseRequestInitializer(FirebaseApp app, @Nullable RetryConfig retryConfig) { + ImmutableList.Builder initializers = + ImmutableList.builder() + .add(new HttpCredentialsAdapter(ImplFirebaseTrampolines.getCredentials(app))) + .add(new TimeoutInitializer(app.getOptions())); + if (retryConfig != null) { + initializers.add(new RetryInitializer(retryConfig)); + } + this.initializers = initializers.build(); } @Override - public void initialize(HttpRequest httpRequest) throws IOException { - credentialsAdapter.initialize(httpRequest); - httpRequest.setConnectTimeout(connectTimeout); - httpRequest.setReadTimeout(readTimeout); + public void initialize(HttpRequest request) throws IOException { + for (HttpRequestInitializer initializer : initializers) { + initializer.initialize(request); + } + } + + private static class TimeoutInitializer implements HttpRequestInitializer { + + private final int connectTimeoutMillis; + private final int readTimeoutMillis; + + TimeoutInitializer(FirebaseOptions options) { + this.connectTimeoutMillis = options.getConnectTimeout(); + this.readTimeoutMillis = options.getReadTimeout(); + } + + @Override + public void initialize(HttpRequest request) { + request.setConnectTimeout(connectTimeoutMillis); + request.setReadTimeout(readTimeoutMillis); + } } } diff --git a/src/main/java/com/google/firebase/internal/RetryConfig.java b/src/main/java/com/google/firebase/internal/RetryConfig.java new file mode 100644 index 000000000..a17780ed0 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/RetryConfig.java @@ -0,0 +1,177 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.util.BackOff; +import com.google.api.client.util.ExponentialBackOff; +import com.google.api.client.util.Sleeper; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Configures when and how HTTP requests should be retried. + */ +public final class RetryConfig { + + private static final int INITIAL_INTERVAL_MILLIS = 500; + + private final List retryStatusCodes; + private final boolean retryOnIOExceptions; + private final int maxRetries; + private final Sleeper sleeper; + private final ExponentialBackOff.Builder backOffBuilder; + + private RetryConfig(Builder builder) { + if (builder.retryStatusCodes != null) { + this.retryStatusCodes = ImmutableList.copyOf(builder.retryStatusCodes); + } else { + this.retryStatusCodes = ImmutableList.of(); + } + + this.retryOnIOExceptions = builder.retryOnIOExceptions; + checkArgument(builder.maxRetries >= 0, "maxRetries must not be negative"); + this.maxRetries = builder.maxRetries; + this.sleeper = checkNotNull(builder.sleeper); + this.backOffBuilder = new ExponentialBackOff.Builder() + .setInitialIntervalMillis(INITIAL_INTERVAL_MILLIS) + .setMaxIntervalMillis(builder.maxIntervalMillis) + .setMultiplier(builder.backOffMultiplier) + .setRandomizationFactor(0); + + // Force validation of arguments by building the BackOff object + this.backOffBuilder.build(); + } + + List getRetryStatusCodes() { + return retryStatusCodes; + } + + boolean isRetryOnIOExceptions() { + return retryOnIOExceptions; + } + + int getMaxRetries() { + return maxRetries; + } + + int getMaxIntervalMillis() { + return backOffBuilder.getMaxIntervalMillis(); + } + + double getBackOffMultiplier() { + return backOffBuilder.getMultiplier(); + } + + Sleeper getSleeper() { + return sleeper; + } + + BackOff newBackOff() { + return backOffBuilder.build(); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + + private List retryStatusCodes; + private boolean retryOnIOExceptions; + private int maxRetries; + private int maxIntervalMillis = (int) TimeUnit.MINUTES.toMillis(2); + private double backOffMultiplier = 2.0; + private Sleeper sleeper = Sleeper.DEFAULT; + + private Builder() { } + + /** + * Sets a list of HTTP status codes that should be retried. If null or empty, HTTP requests + * will not be retried as long as they result in some HTTP response message. + * + * @param retryStatusCodes A list of status codes. + * @return This builder. + */ + public Builder setRetryStatusCodes(List retryStatusCodes) { + this.retryStatusCodes = retryStatusCodes; + return this; + } + + /** + * Sets whether requests should be retried on IOExceptions. + * + * @param retryOnIOExceptions A boolean indicating whether to retry on IOExceptions. + * @return This builder. + */ + public Builder setRetryOnIOExceptions(boolean retryOnIOExceptions) { + this.retryOnIOExceptions = retryOnIOExceptions; + return this; + } + + /** + * Maximum number of retry attempts for a request. This is the cumulative total for all retries + * regardless of their cause (I/O errors and HTTP error responses). + * + * @param maxRetries A non-negative integer. + * @return This builder. + */ + public Builder setMaxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } + + /** + * Maximum interval to wait before a request should be retried. Must be at least 500 + * milliseconds. Defaults to 2 minutes. + * + * @param maxIntervalMillis Interval in milliseconds. + * @return This builder. + */ + public Builder setMaxIntervalMillis(int maxIntervalMillis) { + this.maxIntervalMillis = maxIntervalMillis; + return this; + } + + /** + * Factor by which the retry interval is multiplied when employing exponential back + * off to delay consecutive retries of the same request. Must be at least 1. Defaults + * to 2. + * + * @param backOffMultiplier Multiplication factor for exponential back off. + * @return This builder. + */ + public Builder setBackOffMultiplier(double backOffMultiplier) { + this.backOffMultiplier = backOffMultiplier; + return this; + } + + @VisibleForTesting + Builder setSleeper(Sleeper sleeper) { + this.sleeper = sleeper; + return this; + } + + public RetryConfig build() { + return new RetryConfig(this); + } + } +} diff --git a/src/main/java/com/google/firebase/internal/RetryInitializer.java b/src/main/java/com/google/firebase/internal/RetryInitializer.java new file mode 100644 index 000000000..fbe0a13ea --- /dev/null +++ b/src/main/java/com/google/firebase/internal/RetryInitializer.java @@ -0,0 +1,112 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.HttpBackOffIOExceptionHandler; +import com.google.api.client.http.HttpIOExceptionHandler; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpUnsuccessfulResponseHandler; +import java.io.IOException; + +/** + * Configures HTTP requests to be retried. Requests that encounter I/O errors are retried if + * {@link RetryConfig#isRetryOnIOExceptions()} is set. Requests failing with unsuccessful HTTP + * responses are first referred to the {@code HttpUnsuccessfulResponseHandler} that was originally + * set on the request. If the request does not get retried at that level, + * {@link RetryUnsuccessfulResponseHandler} is used to schedule additional retries. + */ +final class RetryInitializer implements HttpRequestInitializer { + + private final RetryConfig retryConfig; + + RetryInitializer(RetryConfig retryConfig) { + this.retryConfig = checkNotNull(retryConfig); + } + + @Override + public void initialize(HttpRequest request) { + request.setNumberOfRetries(retryConfig.getMaxRetries()); + request.setUnsuccessfulResponseHandler(newUnsuccessfulResponseHandler(request)); + if (retryConfig.isRetryOnIOExceptions()) { + request.setIOExceptionHandler(newIOExceptionHandler()); + } + } + + private HttpUnsuccessfulResponseHandler newUnsuccessfulResponseHandler(HttpRequest request) { + RetryUnsuccessfulResponseHandler retryHandler = new RetryUnsuccessfulResponseHandler( + retryConfig); + return new RetryHandlerDecorator(retryHandler, request); + } + + private HttpIOExceptionHandler newIOExceptionHandler() { + return new HttpBackOffIOExceptionHandler(retryConfig.newBackOff()) + .setSleeper(retryConfig.getSleeper()); + } + + /** + * Makes sure that any error handlers already set on the request are executed before the retry + * handler is called. This is needed since some initializers (e.g. HttpCredentialsAdapter) + * register their own error handlers. + */ + static class RetryHandlerDecorator implements HttpUnsuccessfulResponseHandler { + + private final RetryUnsuccessfulResponseHandler retryHandler; + private final HttpUnsuccessfulResponseHandler preRetryHandler; + + private RetryHandlerDecorator( + RetryUnsuccessfulResponseHandler retryHandler, HttpRequest request) { + this.retryHandler = checkNotNull(retryHandler); + HttpUnsuccessfulResponseHandler preRetryHandler = request.getUnsuccessfulResponseHandler(); + if (preRetryHandler == null) { + preRetryHandler = new HttpUnsuccessfulResponseHandler() { + @Override + public boolean handleResponse( + HttpRequest request, HttpResponse response, boolean supportsRetry) { + return false; + } + }; + } + this.preRetryHandler = preRetryHandler; + } + + @Override + public boolean handleResponse( + HttpRequest request, + HttpResponse response, + boolean supportsRetry) throws IOException { + try { + boolean retry = preRetryHandler.handleResponse(request, response, supportsRetry); + if (!retry) { + retry = retryHandler.handleResponse(request, response, supportsRetry); + } + return retry; + } finally { + // Pre-retry handler may have reset the unsuccessful response handler on the + // request. This changes it back. + request.setUnsuccessfulResponseHandler(this); + } + } + + RetryUnsuccessfulResponseHandler getRetryHandler() { + return retryHandler; + } + } +} diff --git a/src/main/java/com/google/firebase/internal/RetryUnsuccessfulResponseHandler.java b/src/main/java/com/google/firebase/internal/RetryUnsuccessfulResponseHandler.java new file mode 100644 index 000000000..dd00d2bfb --- /dev/null +++ b/src/main/java/com/google/firebase/internal/RetryUnsuccessfulResponseHandler.java @@ -0,0 +1,112 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpUnsuccessfulResponseHandler; +import com.google.api.client.util.BackOff; +import com.google.api.client.util.BackOffUtils; +import com.google.api.client.util.Clock; +import com.google.api.client.util.Sleeper; +import com.google.common.base.Strings; +import java.io.IOException; +import java.util.Date; + +/** + * An {@code HttpUnsuccessfulResponseHandler} that retries failing requests after an interval. The + * interval is determined by checking the Retry-After header on the last response. If that + * header is not present, uses exponential back off to delay subsequent retries. + */ +final class RetryUnsuccessfulResponseHandler implements HttpUnsuccessfulResponseHandler { + + private final RetryConfig retryConfig; + private final BackOff backOff; + private final Sleeper sleeper; + private final Clock clock; + + RetryUnsuccessfulResponseHandler(RetryConfig retryConfig) { + this(retryConfig, Clock.SYSTEM); + } + + RetryUnsuccessfulResponseHandler(RetryConfig retryConfig, Clock clock) { + this.retryConfig = checkNotNull(retryConfig); + this.backOff = retryConfig.newBackOff(); + this.sleeper = retryConfig.getSleeper(); + this.clock = checkNotNull(clock); + } + + @Override + public boolean handleResponse( + HttpRequest request, HttpResponse response, boolean supportsRetry) throws IOException { + + if (!supportsRetry) { + return false; + } + + int statusCode = response.getStatusCode(); + if (!retryConfig.getRetryStatusCodes().contains(statusCode)) { + return false; + } + + try { + return waitAndRetry(response); + } catch (InterruptedException e) { + // ignore + } + return false; + } + + RetryConfig getRetryConfig() { + return retryConfig; + } + + private boolean waitAndRetry(HttpResponse response) throws IOException, InterruptedException { + String retryAfterHeader = response.getHeaders().getRetryAfter(); + if (!Strings.isNullOrEmpty(retryAfterHeader)) { + long intervalMillis = parseRetryAfterHeaderIntoMillis(retryAfterHeader.trim()); + // Retry-after header can specify very long delay intervals (e.g. 24 hours). If we cannot + // wait that long, we should not perform any retries at all. In general it is not correct to + // retry earlier than what the server has recommended to us. + if (intervalMillis > retryConfig.getMaxIntervalMillis()) { + return false; + } + + if (intervalMillis > 0) { + sleeper.sleep(intervalMillis); + return true; + } + } + + return BackOffUtils.next(sleeper, backOff); + } + + private long parseRetryAfterHeaderIntoMillis(String retryAfter) { + try { + return Long.parseLong(retryAfter) * 1000; + } catch (NumberFormatException e) { + Date date = DateUtils.parseDate(retryAfter); + if (date != null) { + return date.getTime() - clock.currentTimeMillis(); + } + } + + return -1L; + } +} diff --git a/src/test/java/com/google/firebase/internal/CountingLowLevelHttpRequest.java b/src/test/java/com/google/firebase/internal/CountingLowLevelHttpRequest.java new file mode 100644 index 000000000..f2a4b9f5a --- /dev/null +++ b/src/test/java/com/google/firebase/internal/CountingLowLevelHttpRequest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import java.io.IOException; +import java.util.Map; + +class CountingLowLevelHttpRequest extends MockLowLevelHttpRequest { + + private final LowLevelHttpResponse response; + private final IOException exception; + private int count; + + private CountingLowLevelHttpRequest(LowLevelHttpResponse response, IOException exception) { + this.response = response; + this.exception = exception; + } + + static CountingLowLevelHttpRequest fromStatus(int status) { + return fromStatus(status, null); + } + + static CountingLowLevelHttpRequest fromStatus(int status, Map headers) { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(status) + .setZeroContent(); + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + response.addHeader(entry.getKey(), entry.getValue()); + } + } + return fromStatus(response); + } + + static CountingLowLevelHttpRequest fromStatus(LowLevelHttpResponse response) { + return new CountingLowLevelHttpRequest(checkNotNull(response), null); + } + + static CountingLowLevelHttpRequest fromException(IOException exception) { + return new CountingLowLevelHttpRequest(null, checkNotNull(exception)); + } + + @Override + public LowLevelHttpResponse execute() throws IOException { + count++; + if (response != null) { + return response; + } + throw exception; + } + + int getCount() { + return count; + } +} diff --git a/src/test/java/com/google/firebase/internal/DateUtilsTest.java b/src/test/java/com/google/firebase/internal/DateUtilsTest.java new file mode 100644 index 000000000..7c2dfd089 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/DateUtilsTest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import java.util.Calendar; +import java.util.Date; +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for the {@link DateUtils}. Adapted from the tests available in the + * + * Apache HTTP client library. + */ +public class DateUtilsTest { + + @Test + public void testBasicDateParse() { + final Calendar calendar = Calendar.getInstance(); + calendar.setTimeZone(DateUtils.GMT); + calendar.set(2005, Calendar.OCTOBER, 14, 0, 0, 0); + calendar.set(Calendar.MILLISECOND, 0); + final Date date1 = calendar.getTime(); + + Date date2 = DateUtils.parseDate("Fri, 14 Oct 2005 00:00:00 GMT"); + Assert.assertEquals(date1, date2); + date2 = DateUtils.parseDate("Fri, 14 Oct 2005 00:00:00 GMT"); + Assert.assertEquals(date1, date2); + date2 = DateUtils.parseDate("Fri, 14 Oct 2005 00:00:00 GMT"); + Assert.assertEquals(date1, date2); + } + + @Test + public void testInvalidInput() { + try { + DateUtils.parseDate(null); + Assert.fail("NullPointerException should have been thrown"); + } catch (NullPointerException ex) { + // expected + } + } + + @Test + public void testTwoDigitYearDateParse() { + final Calendar calendar = Calendar.getInstance(); + calendar.setTimeZone(DateUtils.GMT); + calendar.set(2005, Calendar.OCTOBER, 14, 0, 0, 0); + calendar.set(Calendar.MILLISECOND, 0); + Date date1 = calendar.getTime(); + + Date date2 = DateUtils.parseDate("Friday, 14-Oct-05 00:00:00 GMT"); + Assert.assertEquals(date1, date2); + } + + @Test + public void testParseQuotedDate() { + final Calendar calendar = Calendar.getInstance(); + calendar.setTimeZone(DateUtils.GMT); + calendar.set(2005, Calendar.OCTOBER, 14, 0, 0, 0); + calendar.set(Calendar.MILLISECOND, 0); + final Date date1 = calendar.getTime(); + + final Date date2 = DateUtils.parseDate("'Fri, 14 Oct 2005 00:00:00 GMT'"); + Assert.assertEquals(date1, date2); + } +} diff --git a/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java b/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java index c7919eb97..1f610f5e3 100644 --- a/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java +++ b/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java @@ -17,55 +17,136 @@ package com.google.firebase.internal; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; -import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpBackOffIOExceptionHandler; import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.http.HttpResponseException; +import com.google.auth.http.HttpCredentialsAdapter; 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.TestUtils; import org.junit.After; import org.junit.Test; public class FirebaseRequestInitializerTest { + private static final int MAX_RETRIES = 5; + private static final int CONNECT_TIMEOUT_MILLIS = 30000; + private static final int READ_TIMEOUT_MILLIS = 60000; + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); } @Test - public void testDefaultTimeouts() throws Exception { + public void testDefaultSettings() throws Exception { FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() .setCredentials(new MockGoogleCredentials("token")) .build()); - HttpTransport transport = new MockHttpTransport(); - HttpRequestFactory factory = transport.createRequestFactory( - new FirebaseRequestInitializer(app)); - HttpRequest request = factory.buildGetRequest( - new GenericUrl("https://firebase.google.com")); + HttpRequest request = TestUtils.createRequest(); + + FirebaseRequestInitializer initializer = new FirebaseRequestInitializer(app); + initializer.initialize(request); + + assertEquals(0, request.getConnectTimeout()); assertEquals(0, request.getReadTimeout()); assertEquals("Bearer token", request.getHeaders().getAuthorization()); + assertEquals(HttpRequest.DEFAULT_NUMBER_OF_RETRIES, request.getNumberOfRetries()); + assertNull(request.getIOExceptionHandler()); + assertTrue(request.getUnsuccessfulResponseHandler() instanceof HttpCredentialsAdapter); } @Test public void testExplicitTimeouts() throws Exception { FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() .setCredentials(new MockGoogleCredentials("token")) - .setConnectTimeout(30000) - .setReadTimeout(60000) + .setConnectTimeout(CONNECT_TIMEOUT_MILLIS) + .setReadTimeout(READ_TIMEOUT_MILLIS) .build()); - HttpTransport transport = new MockHttpTransport(); - HttpRequestFactory factory = transport.createRequestFactory( - new FirebaseRequestInitializer(app)); - HttpRequest request = factory.buildGetRequest( - new GenericUrl("https://firebase.google.com")); - assertEquals(30000, request.getConnectTimeout()); - assertEquals(60000, request.getReadTimeout()); + HttpRequest request = TestUtils.createRequest(); + + FirebaseRequestInitializer initializer = new FirebaseRequestInitializer(app); + initializer.initialize(request); + + assertEquals(CONNECT_TIMEOUT_MILLIS, request.getConnectTimeout()); + assertEquals(READ_TIMEOUT_MILLIS, request.getReadTimeout()); + assertEquals("Bearer token", request.getHeaders().getAuthorization()); + assertEquals(HttpRequest.DEFAULT_NUMBER_OF_RETRIES, request.getNumberOfRetries()); + assertNull(request.getIOExceptionHandler()); + assertTrue(request.getUnsuccessfulResponseHandler() instanceof HttpCredentialsAdapter); + } + + @Test + public void testRetryConfig() throws Exception { + FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("token")) + .build()); + RetryConfig retryConfig = RetryConfig.builder() + .setMaxRetries(MAX_RETRIES) + .build(); + HttpRequest request = TestUtils.createRequest(); + + FirebaseRequestInitializer initializer = new FirebaseRequestInitializer(app, retryConfig); + initializer.initialize(request); + + assertEquals(0, request.getConnectTimeout()); + assertEquals(0, request.getReadTimeout()); + assertEquals("Bearer token", request.getHeaders().getAuthorization()); + assertEquals(MAX_RETRIES, request.getNumberOfRetries()); + assertNull(request.getIOExceptionHandler()); + assertNotNull(request.getUnsuccessfulResponseHandler()); + } + + @Test + public void testRetryConfigWithIOExceptionHandling() throws Exception { + FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("token")) + .build()); + RetryConfig retryConfig = RetryConfig.builder() + .setMaxRetries(MAX_RETRIES) + .setRetryOnIOExceptions(true) + .build(); + HttpRequest request = TestUtils.createRequest(); + + FirebaseRequestInitializer initializer = new FirebaseRequestInitializer(app, retryConfig); + initializer.initialize(request); + + assertEquals(0, request.getConnectTimeout()); + assertEquals(0, request.getReadTimeout()); + assertEquals("Bearer token", request.getHeaders().getAuthorization()); + assertEquals(MAX_RETRIES, request.getNumberOfRetries()); + assertTrue(request.getIOExceptionHandler() instanceof HttpBackOffIOExceptionHandler); + assertNotNull(request.getUnsuccessfulResponseHandler()); + } + + @Test + public void testCredentialsRetryHandler() throws Exception { + FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("token")) + .build()); + RetryConfig retryConfig = RetryConfig.builder() + .setMaxRetries(MAX_RETRIES) + .build(); + CountingLowLevelHttpRequest countingRequest = CountingLowLevelHttpRequest.fromStatus(401); + HttpRequest request = TestUtils.createRequest(countingRequest); + FirebaseRequestInitializer initializer = new FirebaseRequestInitializer(app, retryConfig); + initializer.initialize(request); + request.getHeaders().setAuthorization((String) null); + + try { + request.execute(); + } catch (HttpResponseException e) { + assertEquals(401, e.getStatusCode()); + } + assertEquals("Bearer token", request.getHeaders().getAuthorization()); + assertEquals(MAX_RETRIES + 1, countingRequest.getCount()); } } diff --git a/src/test/java/com/google/firebase/internal/RetryConfigTest.java b/src/test/java/com/google/firebase/internal/RetryConfigTest.java new file mode 100644 index 000000000..7fab5075d --- /dev/null +++ b/src/test/java/com/google/firebase/internal/RetryConfigTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.testing.util.MockSleeper; +import com.google.api.client.util.BackOff; +import com.google.api.client.util.ExponentialBackOff; +import com.google.api.client.util.Sleeper; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import org.junit.Test; + +public class RetryConfigTest { + + @Test + public void testEmptyBuilder() { + RetryConfig config = RetryConfig.builder().build(); + + assertTrue(config.getRetryStatusCodes().isEmpty()); + assertEquals(0, config.getMaxRetries()); + assertEquals(2 * 60 * 1000, config.getMaxIntervalMillis()); + assertEquals(2.0, config.getBackOffMultiplier(), 0.01); + assertSame(Sleeper.DEFAULT, config.getSleeper()); + + ExponentialBackOff backOff = (ExponentialBackOff) config.newBackOff(); + assertEquals(2 * 60 * 1000, backOff.getMaxIntervalMillis()); + assertEquals(2.0, backOff.getMultiplier(), 0.01); + assertEquals(500, backOff.getInitialIntervalMillis()); + assertEquals(0.0, backOff.getRandomizationFactor(), 0.01); + assertNotSame(backOff, config.newBackOff()); + } + + @Test + public void testBuilderWithAllSettings() { + ImmutableList statusCodes = ImmutableList.of(500, 503); + Sleeper sleeper = new MockSleeper(); + RetryConfig config = RetryConfig.builder() + .setMaxRetries(4) + .setRetryStatusCodes(statusCodes) + .setRetryOnIOExceptions(true) + .setMaxIntervalMillis(5 * 60 * 1000) + .setBackOffMultiplier(1.5) + .setSleeper(sleeper) + .build(); + + assertEquals(2, config.getRetryStatusCodes().size()); + assertEquals(statusCodes.get(0), config.getRetryStatusCodes().get(0)); + assertEquals(statusCodes.get(1), config.getRetryStatusCodes().get(1)); + assertTrue(config.isRetryOnIOExceptions()); + assertEquals(4, config.getMaxRetries()); + assertEquals(5 * 60 * 1000, config.getMaxIntervalMillis()); + assertEquals(1.5, config.getBackOffMultiplier(), 0.01); + assertSame(sleeper, config.getSleeper()); + + ExponentialBackOff backOff = (ExponentialBackOff) config.newBackOff(); + assertEquals(500, backOff.getInitialIntervalMillis()); + assertEquals(5 * 60 * 1000, backOff.getMaxIntervalMillis()); + assertEquals(1.5, backOff.getMultiplier(), 0.01); + assertEquals(0.0, backOff.getRandomizationFactor(), 0.01); + assertNotSame(backOff, config.newBackOff()); + } + + @Test + public void testExponentialBackOff() throws IOException { + RetryConfig config = RetryConfig.builder() + .setMaxIntervalMillis(12000) + .build(); + + BackOff backOff = config.newBackOff(); + + assertEquals(500, backOff.nextBackOffMillis()); + assertEquals(1000, backOff.nextBackOffMillis()); + assertEquals(2000, backOff.nextBackOffMillis()); + assertEquals(4000, backOff.nextBackOffMillis()); + assertEquals(8000, backOff.nextBackOffMillis()); + assertEquals(12000, backOff.nextBackOffMillis()); + assertEquals(12000, backOff.nextBackOffMillis()); + } + + @Test(expected = IllegalArgumentException.class) + public void testNegativeMaxRetriesNotAllowed() { + RetryConfig.builder() + .setMaxRetries(-1) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testMaxIntervalMillisTooSmall() { + RetryConfig.builder() + .setMaxIntervalMillis(499) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBackOffMultiplierTooSmall() { + RetryConfig.builder() + .setBackOffMultiplier(0.99) + .build(); + } + + @Test(expected = NullPointerException.class) + public void testSleeperCannotBeNull() { + RetryConfig.builder() + .setSleeper(null) + .build(); + } +} diff --git a/src/test/java/com/google/firebase/internal/RetryInitializerTest.java b/src/test/java/com/google/firebase/internal/RetryInitializerTest.java new file mode 100644 index 000000000..8732e8bf5 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/RetryInitializerTest.java @@ -0,0 +1,244 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.http.HttpBackOffIOExceptionHandler; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpUnsuccessfulResponseHandler; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.api.client.testing.util.MockSleeper; +import com.google.api.client.util.Sleeper; +import com.google.auth.http.HttpCredentialsAdapter; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.RetryInitializer.RetryHandlerDecorator; +import com.google.firebase.testing.TestUtils; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; + +public class RetryInitializerTest { + + private static final int MAX_RETRIES = 4; + + @Test + public void testEnableRetry() throws IOException { + RetryConfig retryConfig = retryOnIOAndServiceUnavailableErrors(new MockSleeper()); + RetryInitializer initializer = new RetryInitializer(retryConfig); + HttpRequest request = TestUtils.createRequest(); + + initializer.initialize(request); + + assertEquals(MAX_RETRIES, request.getNumberOfRetries()); + assertTrue(request.getUnsuccessfulResponseHandler() instanceof RetryHandlerDecorator); + RetryUnsuccessfulResponseHandler retryHandler = + ((RetryHandlerDecorator) request.getUnsuccessfulResponseHandler()).getRetryHandler(); + assertSame(retryConfig, retryHandler.getRetryConfig()); + assertTrue(request.getIOExceptionHandler() instanceof HttpBackOffIOExceptionHandler); + } + + @Test + public void testRetryOnIOExceptionDisabled() throws IOException { + RetryInitializer initializer = new RetryInitializer(RetryConfig.builder() + .setMaxRetries(MAX_RETRIES) + .setRetryOnIOExceptions(false) + .setRetryStatusCodes(ImmutableList.of(503)) + .build()); + HttpRequest request = TestUtils.createRequest(); + + initializer.initialize(request); + + assertEquals(MAX_RETRIES, request.getNumberOfRetries()); + assertNotNull(request.getUnsuccessfulResponseHandler()); + assertNull(request.getIOExceptionHandler()); + } + + @Test(expected = NullPointerException.class) + public void testRetryConfigCannotBeNull() { + new RetryInitializer(null); + } + + @Test + public void testRetryOnIOException() throws IOException { + MockSleeper sleeper = new MockSleeper(); + RetryInitializer initializer = new RetryInitializer( + retryOnIOAndServiceUnavailableErrors(sleeper)); + + CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromException( + new IOException("test error")); + HttpRequest request = TestUtils.createRequest(failingRequest); + initializer.initialize(request); + final HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + + try { + request.execute(); + fail("No exception thrown for HTTP error"); + } catch (IOException e) { + assertEquals("test error", e.getMessage()); + } + + assertEquals(MAX_RETRIES, sleeper.getCount()); + assertEquals(MAX_RETRIES + 1, failingRequest.getCount()); + assertSame(retryHandler, request.getUnsuccessfulResponseHandler()); + } + + @Test + public void testRetryOnHttpError() throws IOException { + MockSleeper sleeper = new MockSleeper(); + RetryInitializer initializer = new RetryInitializer( + retryOnIOAndServiceUnavailableErrors(sleeper)); + CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(503); + HttpRequest request = TestUtils.createRequest(failingRequest); + initializer.initialize(request); + final HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + + try { + request.execute(); + fail("No exception thrown for HTTP error"); + } catch (HttpResponseException e) { + assertEquals(503, e.getStatusCode()); + } + + assertEquals(MAX_RETRIES, sleeper.getCount()); + assertEquals(MAX_RETRIES + 1, failingRequest.getCount()); + assertSame(retryHandler, request.getUnsuccessfulResponseHandler()); + } + + @Test + public void testMaxRetriesCountIsCumulative() throws IOException { + MockSleeper sleeper = new MockSleeper(); + RetryInitializer initializer = new RetryInitializer( + retryOnIOAndServiceUnavailableErrors(sleeper)); + + final AtomicInteger counter = new AtomicInteger(0); + MockLowLevelHttpRequest failingRequest = new MockLowLevelHttpRequest(){ + @Override + public LowLevelHttpResponse execute() throws IOException { + if (counter.getAndIncrement() < 2) { + throw new IOException("test error"); + } else { + return new MockLowLevelHttpResponse().setStatusCode(503).setZeroContent(); + } + } + }; + HttpRequest request = TestUtils.createRequest(failingRequest); + initializer.initialize(request); + final HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + + try { + request.execute(); + fail("No exception thrown for HTTP error"); + } catch (HttpResponseException e) { + assertEquals(503, e.getStatusCode()); + } + + assertEquals(MAX_RETRIES, sleeper.getCount()); + assertEquals(MAX_RETRIES + 1, counter.get()); + assertSame(retryHandler, request.getUnsuccessfulResponseHandler()); + } + + @Test + public void testOtherErrorHandlersCalledBeforeRetry() throws IOException { + final AtomicInteger otherErrorHandlerCalls = new AtomicInteger(0); + HttpCredentialsAdapter credentials = new HttpCredentialsAdapter(new MockGoogleCredentials()) { + @Override + public boolean handleResponse( + HttpRequest request, HttpResponse response, boolean supportsRetry) { + otherErrorHandlerCalls.incrementAndGet(); + return super.handleResponse(request, response, supportsRetry); + } + }; + MockSleeper sleeper = new MockSleeper(); + RetryInitializer initializer = new RetryInitializer(RetryConfig.builder() + .setMaxRetries(MAX_RETRIES) + .setRetryStatusCodes(ImmutableList.of(503)) + .setSleeper(sleeper) + .build()); + CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(503); + HttpRequest request = TestUtils.createRequest(failingRequest); + credentials.initialize(request); + initializer.initialize(request); + final HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + + try { + request.execute(); + fail("No exception thrown for HTTP error"); + } catch (HttpResponseException e) { + assertEquals(503, e.getStatusCode()); + } + + assertEquals(MAX_RETRIES, sleeper.getCount()); + assertEquals(MAX_RETRIES + 1, failingRequest.getCount()); + assertEquals(MAX_RETRIES + 1, otherErrorHandlerCalls.get()); + assertSame(retryHandler, request.getUnsuccessfulResponseHandler()); + } + + @Test + public void testRetryHandlerDoesNotGetOverwritten() throws IOException { + final AtomicInteger otherErrorHandlerCalls = new AtomicInteger(0); + HttpUnsuccessfulResponseHandler credentials = new HttpUnsuccessfulResponseHandler() { + @Override + public boolean handleResponse( + HttpRequest request, HttpResponse response, boolean supportsRetry) throws IOException { + otherErrorHandlerCalls.incrementAndGet(); + request.setUnsuccessfulResponseHandler(this); + throw new IOException("test"); + } + }; + MockSleeper sleeper = new MockSleeper(); + RetryInitializer initializer = new RetryInitializer(RetryConfig.builder() + .setMaxRetries(MAX_RETRIES) + .setRetryStatusCodes(ImmutableList.of(503)) + .setSleeper(sleeper) + .build()); + CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(503); + HttpRequest request = TestUtils.createRequest(failingRequest); + request.setUnsuccessfulResponseHandler(credentials); + initializer.initialize(request); + final HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + + try { + request.execute(); + fail("No exception thrown for HTTP error"); + } catch (Exception e) { + assertEquals("test", e.getMessage()); + } + + assertEquals(1, otherErrorHandlerCalls.get()); + assertSame(retryHandler, request.getUnsuccessfulResponseHandler()); + } + + private RetryConfig retryOnIOAndServiceUnavailableErrors(Sleeper sleeper) { + return RetryConfig.builder() + .setMaxRetries(MAX_RETRIES) + .setRetryStatusCodes(ImmutableList.of(503)) + .setRetryOnIOExceptions(true) + .setSleeper(sleeper) + .build(); + } +} diff --git a/src/test/java/com/google/firebase/internal/RetryUnsuccessfulResponseHandlerTest.java b/src/test/java/com/google/firebase/internal/RetryUnsuccessfulResponseHandlerTest.java new file mode 100644 index 000000000..077e9e52f --- /dev/null +++ b/src/test/java/com/google/firebase/internal/RetryUnsuccessfulResponseHandlerTest.java @@ -0,0 +1,258 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.testing.http.FixedClock; +import com.google.api.client.testing.util.MockSleeper; +import com.google.api.client.util.Clock; +import com.google.api.client.util.Sleeper; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.Longs; +import com.google.firebase.testing.TestUtils; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; +import org.junit.Test; + +public class RetryUnsuccessfulResponseHandlerTest { + + private static final int MAX_RETRIES = 4; + private static final RetryConfig.Builder TEST_RETRY_CONFIG = RetryConfig.builder() + .setRetryStatusCodes(ImmutableList.of(429, 503)) + .setMaxIntervalMillis(120 * 1000); + + @Test + public void testDoesNotRetryOnUnspecifiedHttpStatus() throws IOException { + MultipleCallSleeper sleeper = new MultipleCallSleeper(); + RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler( + testRetryConfig(sleeper)); + CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(404); + HttpRequest request = TestUtils.createRequest(failingRequest); + request.setUnsuccessfulResponseHandler(handler); + request.setNumberOfRetries(MAX_RETRIES); + + try { + request.execute(); + fail("No exception thrown for HTTP error"); + } catch (HttpResponseException e) { + assertEquals(404, e.getStatusCode()); + } + + assertEquals(0, sleeper.getCount()); + assertEquals(1, failingRequest.getCount()); + } + + @Test + public void testRetryOnHttpClientErrorWhenSpecified() throws IOException { + MultipleCallSleeper sleeper = new MultipleCallSleeper(); + RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler( + testRetryConfig(sleeper)); + CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(429); + HttpRequest request = TestUtils.createRequest(failingRequest); + request.setUnsuccessfulResponseHandler(handler); + request.setNumberOfRetries(MAX_RETRIES); + + try { + request.execute(); + fail("No exception thrown for HTTP error"); + } catch (HttpResponseException e) { + assertEquals(429, e.getStatusCode()); + } + + assertEquals(MAX_RETRIES, sleeper.getCount()); + assertArrayEquals(new long[]{500, 1000, 2000, 4000}, sleeper.getDelays()); + assertEquals(MAX_RETRIES + 1, failingRequest.getCount()); + } + + @Test + public void testExponentialBackOffDoesNotExceedMaxInterval() throws IOException { + MultipleCallSleeper sleeper = new MultipleCallSleeper(); + RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler( + testRetryConfig(sleeper)); + CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(503); + HttpRequest request = TestUtils.createRequest(failingRequest); + request.setUnsuccessfulResponseHandler(handler); + request.setNumberOfRetries(10); + + try { + request.execute(); + fail("No exception thrown for HTTP error"); + } catch (HttpResponseException e) { + assertEquals(503, e.getStatusCode()); + } + + assertEquals(10, sleeper.getCount()); + assertArrayEquals( + new long[]{500, 1000, 2000, 4000, 8000, 16000, 32000, 64000, 120000, 120000}, + sleeper.getDelays()); + assertEquals(11, failingRequest.getCount()); + } + + @Test + public void testRetryAfterGivenAsSeconds() throws IOException { + MultipleCallSleeper sleeper = new MultipleCallSleeper(); + RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler( + testRetryConfig(sleeper)); + CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus( + 503, ImmutableMap.of("retry-after", "2")); + HttpRequest request = TestUtils.createRequest(failingRequest); + request.setUnsuccessfulResponseHandler(handler); + request.setNumberOfRetries(MAX_RETRIES); + + try { + request.execute(); + fail("No exception thrown for HTTP error"); + } catch (HttpResponseException e) { + assertEquals(503, e.getStatusCode()); + } + + assertEquals(MAX_RETRIES, sleeper.getCount()); + assertArrayEquals(new long[]{2000, 2000, 2000, 2000}, sleeper.getDelays()); + assertEquals(MAX_RETRIES + 1, failingRequest.getCount()); + } + + @Test + public void testRetryAfterGivenAsDate() throws IOException { + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz"); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + Date date = new Date(1000); + Clock clock = new FixedClock(date.getTime()); + String retryAfter = dateFormat.format(new Date(date.getTime() + 30000)); + + MultipleCallSleeper sleeper = new MultipleCallSleeper(); + RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler( + testRetryConfig(sleeper), clock); + CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus( + 503, ImmutableMap.of("retry-after", retryAfter)); + HttpRequest request = TestUtils.createRequest(failingRequest); + request.setUnsuccessfulResponseHandler(handler); + request.setNumberOfRetries(4); + + try { + request.execute(); + fail("No exception thrown for HTTP error"); + } catch (HttpResponseException e) { + assertEquals(503, e.getStatusCode()); + } + + assertEquals(4, sleeper.getCount()); + assertArrayEquals(new long[]{30000, 30000, 30000, 30000}, sleeper.getDelays()); + assertEquals(5, failingRequest.getCount()); + } + + @Test + public void testInvalidRetryAfterFailsOverToExpBackOff() throws IOException { + MultipleCallSleeper sleeper = new MultipleCallSleeper(); + RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler( + testRetryConfig(sleeper)); + CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus( + 503, ImmutableMap.of("retry-after", "not valid")); + HttpRequest request = TestUtils.createRequest(failingRequest); + request.setUnsuccessfulResponseHandler(handler); + request.setNumberOfRetries(4); + + try { + request.execute(); + fail("No exception thrown for HTTP error"); + } catch (HttpResponseException e) { + assertEquals(503, e.getStatusCode()); + } + + assertEquals(4, sleeper.getCount()); + assertArrayEquals(new long[]{500, 1000, 2000, 4000}, sleeper.getDelays()); + assertEquals(5, failingRequest.getCount()); + } + + @Test + public void testDoesNotRetryWhenRetryAfterIsTooLong() throws IOException { + MultipleCallSleeper sleeper = new MultipleCallSleeper(); + RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler( + testRetryConfig(sleeper)); + CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus( + 503, ImmutableMap.of("retry-after", "121")); + HttpRequest request = TestUtils.createRequest(failingRequest); + request.setUnsuccessfulResponseHandler(handler); + request.setNumberOfRetries(MAX_RETRIES); + + try { + request.execute(); + fail("No exception thrown for HTTP error"); + } catch (HttpResponseException e) { + assertEquals(503, e.getStatusCode()); + } + + assertEquals(0, sleeper.getCount()); + assertEquals(1, failingRequest.getCount()); + } + + @Test + public void testDoesNotRetryAfterInterruption() throws IOException { + MockSleeper sleeper = new MockSleeper() { + @Override + public void sleep(long millis) throws InterruptedException { + super.sleep(millis); + throw new InterruptedException(); + } + }; + RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler( + testRetryConfig(sleeper)); + CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(503); + HttpRequest request = TestUtils.createRequest(failingRequest); + request.setUnsuccessfulResponseHandler(handler); + request.setNumberOfRetries(MAX_RETRIES); + + try { + request.execute(); + fail("No exception thrown for HTTP error"); + } catch (HttpResponseException e) { + assertEquals(503, e.getStatusCode()); + } + + assertEquals(1, sleeper.getCount()); + assertEquals(1, failingRequest.getCount()); + } + + private RetryConfig testRetryConfig(Sleeper sleeper) { + return TEST_RETRY_CONFIG.setSleeper(sleeper).build(); + } + + + private static class MultipleCallSleeper extends MockSleeper { + + private final List delays = new ArrayList<>(); + + @Override + public void sleep(long millis) throws InterruptedException { + super.sleep(millis); + delays.add(millis); + } + + long[] getDelays() { + return Longs.toArray(delays); + } + } +} diff --git a/src/test/java/com/google/firebase/testing/TestUtils.java b/src/test/java/com/google/firebase/testing/TestUtils.java index 0dec4db3f..3e33a52ea 100644 --- a/src/test/java/com/google/firebase/testing/TestUtils.java +++ b/src/test/java/com/google/firebase/testing/TestUtils.java @@ -19,8 +19,14 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.client.googleapis.testing.auth.oauth2.MockTokenServerTransport; +import com.google.api.client.http.EmptyContent; +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.HttpTransport; import com.google.api.client.json.webtoken.JsonWebSignature; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.GoogleCredentials; import com.google.common.collect.ImmutableMap; @@ -41,7 +47,8 @@ public class TestUtils { public static final long TEST_TIMEOUT_MILLIS = 7 * 1000; - public static final String TEST_ADC_ACCESS_TOKEN = "test-adc-access-token"; + private static final String TEST_ADC_ACCESS_TOKEN = "test-adc-access-token"; + private static final GenericUrl TEST_URL = new GenericUrl("https://firebase.google.com"); private static GoogleCredentials defaultCredentials; @@ -123,4 +130,16 @@ public HttpTransport create() { }); return defaultCredentials; } + + public static HttpRequest createRequest() throws IOException { + return createRequest(new MockLowLevelHttpRequest()); + } + + public static HttpRequest createRequest(MockLowLevelHttpRequest request) throws IOException { + HttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpRequest(request) + .build(); + HttpRequestFactory requestFactory = transport.createRequestFactory(); + return requestFactory.buildPostRequest(TEST_URL, new EmptyContent()); + } } From ca75a0f8df1edccbaba971f2f336b2e941171ba5 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Tue, 19 Mar 2019 02:14:27 +0800 Subject: [PATCH 058/456] Reject non-standard Number classes during POJO serialization (RTDB) (#262) * Reject non-standard Number classes during POJO serialization * Removing string concatenation where unnecessary --- .../utilities/encoding/CustomClassMapper.java | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/google/firebase/database/utilities/encoding/CustomClassMapper.java b/src/main/java/com/google/firebase/database/utilities/encoding/CustomClassMapper.java index 5b14fc295..c5ef85e4a 100644 --- a/src/main/java/com/google/firebase/database/utilities/encoding/CustomClassMapper.java +++ b/src/main/java/com/google/firebase/database/utilities/encoding/CustomClassMapper.java @@ -123,20 +123,20 @@ private static Object serialize(T obj) { return ((Number) obj).longValue(); } return doubleValue; - } else if (obj instanceof Short) { - throw new DatabaseException("Shorts are not supported, please use int or long"); - } else if (obj instanceof Byte) { - throw new DatabaseException("Bytes are not supported, please use int or long"); - } else { - // Long, Integer + } else if (obj instanceof Long || obj instanceof Integer) { return obj; + } else { + throw new DatabaseException( + String.format( + "Numbers of type %s are not supported, please use an int, long, float or double", + obj.getClass().getSimpleName())); } } else if (obj instanceof String) { return obj; } else if (obj instanceof Boolean) { return obj; } else if (obj instanceof Character) { - throw new DatabaseException("Characters are not supported, please strings"); + throw new DatabaseException("Characters are not supported, please use Strings"); } else if (obj instanceof Map) { Map result = new HashMap<>(); for (Map.Entry entry : ((Map) obj).entrySet()) { @@ -159,11 +159,11 @@ private static Object serialize(T obj) { return result; } else { throw new DatabaseException( - "Serializing Collections is not supported, " + "please use Lists instead"); + "Serializing Collections is not supported, please use Lists instead"); } } else if (obj.getClass().isArray()) { throw new DatabaseException( - "Serializing Arrays is not supported, please use Lists " + "instead"); + "Serializing Arrays is not supported, please use Lists instead"); } else if (obj instanceof Enum) { return ((Enum) obj).name(); } else { @@ -185,7 +185,7 @@ private static T deserializeToType(Object obj, Type type) { throw new DatabaseException("Generic wildcard types are not supported"); } else if (type instanceof GenericArrayType) { throw new DatabaseException( - "Generic Arrays are not supported, please use Lists " + "instead"); + "Generic Arrays are not supported, please use Lists instead"); } else { throw new IllegalStateException("Unknown type encountered: " + type); } @@ -204,7 +204,7 @@ private static T deserializeToClass(Object obj, Class clazz) { return (T) convertString(obj); } else if (clazz.isArray()) { throw new DatabaseException( - "Converting to Arrays is not supported, please use Lists" + "instead"); + "Converting to Arrays is not supported, please use Lists instead"); } else if (clazz.getTypeParameters().length > 0) { throw new DatabaseException( "Class " @@ -282,14 +282,9 @@ private static T deserializeToPrimitive(Object obj, Class clazz) { return (T) convertLong(obj); } else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) { return (T) (Float) convertDouble(obj).floatValue(); - } else if (Short.class.isAssignableFrom(clazz) || short.class.isAssignableFrom(clazz)) { - throw new DatabaseException("Deserializing to shorts is not supported"); - } else if (Byte.class.isAssignableFrom(clazz) || byte.class.isAssignableFrom(clazz)) { - throw new DatabaseException("Deserializing to bytes is not supported"); - } else if (Character.class.isAssignableFrom(clazz) || char.class.isAssignableFrom(clazz)) { - throw new DatabaseException("Deserializing to char is not supported"); } else { - throw new IllegalArgumentException("Unknown primitive type: " + clazz); + throw new DatabaseException( + String.format("Deserializing values to %s is not supported", clazz.getSimpleName())); } } @@ -716,7 +711,7 @@ public T deserialize(Map values, Map>, Typ Method setter = this.setters.get(propertyName); Type[] params = setter.getGenericParameterTypes(); if (params.length != 1) { - throw new IllegalStateException("Setter does not have exactly one " + "parameter"); + throw new IllegalStateException("Setter does not have exactly one parameter"); } Type resolvedType = resolveType(params[0], types); Object value = CustomClassMapper.deserializeToType(entry.getValue(), resolvedType); From 7e245d0b00e72ecf2213c81f58a584ad527a6452 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 29 Mar 2019 13:35:06 -0700 Subject: [PATCH 059/456] Added X-Firebase-Client header to FCM API calls (#264) --- .../google/firebase/messaging/FirebaseMessagingClient.java | 4 ++-- .../com/google/firebase/messaging/FirebaseMessagingTest.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java index 5e694ca4d..6550852d8 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java @@ -78,7 +78,7 @@ final class FirebaseMessagingClient { private final HttpRequestFactory childRequestFactory; private final JsonFactory jsonFactory; private final HttpResponseInterceptor responseInterceptor; - private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); + private final String clientVersion = "fire-admin-java/" + SdkUtils.getVersion(); FirebaseMessagingClient(FirebaseApp app, @Nullable HttpResponseInterceptor responseInterceptor) { String projectId = ImplFirebaseTrampolines.getProjectId(app); @@ -168,7 +168,7 @@ private BatchRequest newBatchRequest( private void setCommonFcmHeaders(HttpHeaders headers) { headers.set("X-GOOG-API-FORMAT-VERSION", "2"); - headers.set("X-Client-Version", clientVersion); + headers.set("X-Firebase-Client", clientVersion); } private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 3c9b7c4fb..7a7fbb27e 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -983,7 +983,7 @@ private void checkRequestHeader(HttpRequest request) { HttpHeaders headers = request.getHeaders(); assertEquals("Bearer test-token", headers.getAuthorization()); assertEquals("2", headers.get("X-GOOG-API-FORMAT-VERSION")); - assertEquals("Java/Admin/" + SdkUtils.getVersion(), headers.get("X-Client-Version")); + assertEquals("fire-admin-java/" + SdkUtils.getVersion(), headers.get("X-Firebase-Client")); } private FirebaseMessaging getMessagingForBatchRequest( @@ -1056,7 +1056,7 @@ private void checkBatchRequest(HttpRequest request, int expectedParts) throws IO assertEquals(expectedParts, countLinesWithPrefix(lines, "POST " + TEST_FCM_URL)); assertEquals(expectedParts, countLinesWithPrefix(lines, "x-goog-api-format-version: 2")); assertEquals(expectedParts, countLinesWithPrefix( - lines, "x-client-version: Java/Admin/" + SdkUtils.getVersion())); + lines, "x-firebase-client: fire-admin-java/" + SdkUtils.getVersion())); } private int countLinesWithPrefix(String[] lines, String prefix) { From ee96e7592334cb58703f05afc847250012c51454 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 18 Apr 2019 14:40:10 -0700 Subject: [PATCH 060/456] GCB Integration (Experimental) (#265) * Adding GCB config * Fixing test case for GCB * Adding an official mvn image * Additional steps for verbose output * Maven verbose mode * Trigger build --- cloudbuild.yaml | 4 ++++ .../google/firebase/auth/FirebaseCustomTokenTest.java | 10 ++++++++++ 2 files changed, 14 insertions(+) create mode 100644 cloudbuild.yaml diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 000000000..77e86375e --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,4 @@ +steps: + - name: 'maven:3-jdk-7' + entrypoint: 'mvn' + args: ['test', '-V', '-B'] diff --git a/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java b/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java index 17a99c3f3..b277dce14 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java @@ -22,6 +22,8 @@ import static org.junit.Assert.fail; import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpRequest; import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; @@ -36,6 +38,8 @@ import com.google.firebase.database.MapBuilder; import com.google.firebase.testing.MultiRequestMockHttpTransport; import com.google.firebase.testing.ServiceAccount; +import com.google.firebase.testing.TestUtils; +import java.io.IOException; import java.util.List; import org.junit.After; import org.junit.Assert; @@ -145,6 +149,12 @@ public void testNoServiceAccount() throws Exception { FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project-id") + .setHttpTransport(new HttpTransport() { + @Override + protected LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + throw new IOException("transport error"); + } + }) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); try { From c9c9aacad69d1404110a516455bdc4522e9f2388 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 18 Apr 2019 14:45:30 -0700 Subject: [PATCH 061/456] Refactoring the FCM API (#261) * Introduced FirebaseCloudMessaging interface * Improving FCM test coverage * More unit tests * Made InstanceIdClient into an interface * Complete test coverage for FCM * Cleaned up the tests * Fixing a failing test * Cleaned up the test code further by adding helper methods * Cleaning up repetitive test setup code * Adding missing license headers --- .../firebase/messaging/FirebaseMessaging.java | 91 +- .../messaging/FirebaseMessagingClient.java | 259 +-- .../FirebaseMessagingClientImpl.java | 330 ++++ .../firebase/messaging/InstanceIdClient.java | 176 +- .../messaging/InstanceIdClientImpl.java | 192 +++ .../FirebaseMessagingClientImplTest.java | 819 +++++++++ .../messaging/FirebaseMessagingTest.java | 1494 ++++++----------- .../messaging/InstanceIdClientImplTest.java | 400 +++++ .../google/firebase/testing/TestUtils.java | 10 + 9 files changed, 2339 insertions(+), 1432 deletions(-) create mode 100644 src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java create mode 100644 src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java create mode 100644 src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java create mode 100644 src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 49b85b27b..7330c27d3 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -19,17 +19,17 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.core.ApiFuture; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; -import com.google.firebase.internal.Nullable; import java.util.List; @@ -46,18 +46,13 @@ public class FirebaseMessaging { static final String UNKNOWN_ERROR = "unknown-error"; private final FirebaseApp app; - private final FirebaseMessagingClient messagingClient; - private final InstanceIdClient instanceIdClient; + private final Supplier messagingClient; + private final Supplier instanceIdClient; - private FirebaseMessaging(FirebaseApp app) { - this(app, null); - } - - @VisibleForTesting - FirebaseMessaging(FirebaseApp app, @Nullable HttpResponseInterceptor responseInterceptor) { - this.app = checkNotNull(app, "app must not be null"); - this.messagingClient = new FirebaseMessagingClient(app, responseInterceptor); - this.instanceIdClient = new InstanceIdClient(app, responseInterceptor); + private FirebaseMessaging(Builder builder) { + this.app = checkNotNull(builder.firebaseApp); + this.messagingClient = Suppliers.memoize(builder.messagingClient); + this.instanceIdClient = Suppliers.memoize(builder.instanceIdClient); } /** @@ -137,6 +132,7 @@ public ApiFuture sendAsync(@NonNull Message message, boolean dryRun) { private CallableOperation sendOp( final Message message, final boolean dryRun) { checkNotNull(message, "message must not be null"); + final FirebaseMessagingClient messagingClient = getMessagingClient(); return new CallableOperation() { @Override protected String execute() throws FirebaseMessagingException { @@ -290,6 +286,7 @@ private CallableOperation sendAllOp( checkArgument(!immutableMessages.isEmpty(), "messages list must not be empty"); checkArgument(immutableMessages.size() <= 100, "messages list must not contain more than 100 elements"); + final FirebaseMessagingClient messagingClient = getMessagingClient(); return new CallableOperation() { @Override protected BatchResponse execute() throws FirebaseMessagingException { @@ -298,6 +295,11 @@ protected BatchResponse execute() throws FirebaseMessagingException { }; } + @VisibleForTesting + FirebaseMessagingClient getMessagingClient() { + return messagingClient.get(); + } + /** * Subscribes a list of registration tokens to a topic. * @@ -328,6 +330,7 @@ private CallableOperation s final List registrationTokens, final String topic) { checkRegistrationTokens(registrationTokens); checkTopic(topic); + final InstanceIdClient instanceIdClient = getInstanceIdClient(); return new CallableOperation() { @Override protected TopicManagementResponse execute() throws FirebaseMessagingException { @@ -367,6 +370,7 @@ private CallableOperation u final List registrationTokens, final String topic) { checkRegistrationTokens(registrationTokens); checkTopic(topic); + final InstanceIdClient instanceIdClient = getInstanceIdClient(); return new CallableOperation() { @Override protected TopicManagementResponse execute() throws FirebaseMessagingException { @@ -375,7 +379,12 @@ protected TopicManagementResponse execute() throws FirebaseMessagingException { }; } - private static void checkRegistrationTokens(List registrationTokens) { + @VisibleForTesting + InstanceIdClient getInstanceIdClient() { + return this.instanceIdClient.get(); + } + + private void checkRegistrationTokens(List registrationTokens) { checkArgument(registrationTokens != null && !registrationTokens.isEmpty(), "registrationTokens list must not be null or empty"); checkArgument(registrationTokens.size() <= 1000, @@ -386,7 +395,7 @@ private static void checkRegistrationTokens(List registrationTokens) { } } - private static void checkTopic(String topic) { + private void checkTopic(String topic) { checkArgument(!Strings.isNullOrEmpty(topic), "topic must not be null or empty"); checkArgument(topic.matches("^(/topics/)?(private/)?[a-zA-Z0-9-_.~%]+$"), "invalid topic name"); } @@ -396,7 +405,7 @@ private static void checkTopic(String topic) { private static class FirebaseMessagingService extends FirebaseService { FirebaseMessagingService(FirebaseApp app) { - super(SERVICE_ID, new FirebaseMessaging(app)); + super(SERVICE_ID, FirebaseMessaging.fromApp(app)); } @Override @@ -406,4 +415,54 @@ public void destroy() { // which will throw once the app is deleted. } } + + private static FirebaseMessaging fromApp(final FirebaseApp app) { + return FirebaseMessaging.builder() + .setFirebaseApp(app) + .setMessagingClient(new Supplier() { + @Override + public FirebaseMessagingClient get() { + return FirebaseMessagingClientImpl.fromApp(app); + } + }) + .setInstanceIdClient(new Supplier() { + @Override + public InstanceIdClientImpl get() { + return InstanceIdClientImpl.fromApp(app); + } + }) + .build(); + } + + static Builder builder() { + return new Builder(); + } + + static class Builder { + + private FirebaseApp firebaseApp; + private Supplier messagingClient; + private Supplier instanceIdClient; + + private Builder() { } + + Builder setFirebaseApp(FirebaseApp firebaseApp) { + this.firebaseApp = firebaseApp; + return this; + } + + Builder setMessagingClient(Supplier messagingClient) { + this.messagingClient = messagingClient; + return this; + } + + Builder setInstanceIdClient(Supplier instanceIdClient) { + this.instanceIdClient = instanceIdClient; + return this; + } + + FirebaseMessaging build() { + return new FirebaseMessaging(this); + } + } } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java index 6550852d8..da049565d 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClient.java @@ -1,243 +1,32 @@ -/* - * Copyright 2019 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.api.client.googleapis.batch.BatchCallback; -import com.google.api.client.googleapis.batch.BatchRequest; -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.HttpRequestFactory; -import com.google.api.client.http.HttpRequestInitializer; -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.api.client.json.JsonParser; -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.ApiClientUtils; -import com.google.firebase.internal.Nullable; -import com.google.firebase.internal.SdkUtils; -import com.google.firebase.messaging.internal.MessagingServiceErrorResponse; -import com.google.firebase.messaging.internal.MessagingServiceResponse; -import java.io.IOException; import java.util.List; -import java.util.Map; /** - * A helper class for interacting with Firebase Cloud Messaging service. + * An interface for sending Firebase Cloud Messaging (FCM) messages. */ -final class FirebaseMessagingClient { - - private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; - - private static final String FCM_BATCH_URL = "https://fcm.googleapis.com/batch"; - - private static final Map FCM_ERROR_CODES = - ImmutableMap.builder() - // FCM v1 canonical error codes - .put("NOT_FOUND", "registration-token-not-registered") - .put("PERMISSION_DENIED", "mismatched-credential") - .put("RESOURCE_EXHAUSTED", "message-rate-exceeded") - .put("UNAUTHENTICATED", "invalid-apns-credentials") - - // FCM v1 new error codes - .put("APNS_AUTH_ERROR", "invalid-apns-credentials") - .put("INTERNAL", FirebaseMessaging.INTERNAL_ERROR) - .put("INVALID_ARGUMENT", "invalid-argument") - .put("QUOTA_EXCEEDED", "message-rate-exceeded") - .put("SENDER_ID_MISMATCH", "mismatched-credential") - .put("UNAVAILABLE", "server-unavailable") - .put("UNREGISTERED", "registration-token-not-registered") - .build(); - - private final String fcmSendUrl; - private final HttpRequestFactory requestFactory; - private final HttpRequestFactory childRequestFactory; - private final JsonFactory jsonFactory; - private final HttpResponseInterceptor responseInterceptor; - private final String clientVersion = "fire-admin-java/" + SdkUtils.getVersion(); - - FirebaseMessagingClient(FirebaseApp app, @Nullable HttpResponseInterceptor responseInterceptor) { - String projectId = ImplFirebaseTrampolines.getProjectId(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 GOOGLE_CLOUD_PROJECT environment variable."); - this.fcmSendUrl = String.format(FCM_URL, projectId); - this.requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); - this.childRequestFactory = ApiClientUtils.newUnauthorizedRequestFactory(app); - this.jsonFactory = app.getOptions().getJsonFactory(); - this.responseInterceptor = responseInterceptor; - } - - String send(Message message, boolean dryRun) throws FirebaseMessagingException { - try { - return sendSingleRequest(message, dryRun); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); - } - } - - BatchResponse sendAll( - List messages, boolean dryRun) throws FirebaseMessagingException { - try { - return sendBatchRequest(messages, dryRun); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); - } - } - - private String sendSingleRequest(Message message, boolean dryRun) throws IOException { - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(fcmSendUrl), - new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); - setCommonFcmHeaders(request.getHeaders()); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(responseInterceptor); - HttpResponse response = request.execute(); - try { - MessagingServiceResponse parsed = new MessagingServiceResponse(); - jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); - return parsed.getMessageId(); - } finally { - ApiClientUtils.disconnectQuietly(response); - } - } - - private BatchResponse sendBatchRequest( - List messages, boolean dryRun) throws IOException { - - MessagingBatchCallback callback = new MessagingBatchCallback(); - BatchRequest batch = newBatchRequest(messages, dryRun, callback); - batch.execute(); - return new BatchResponse(callback.getResponses()); - } - - private BatchRequest newBatchRequest( - List messages, boolean dryRun, MessagingBatchCallback callback) throws IOException { - - BatchRequest batch = new BatchRequest( - requestFactory.getTransport(), getBatchRequestInitializer()); - batch.setBatchUrl(new GenericUrl(FCM_BATCH_URL)); - - final JsonObjectParser jsonParser = new JsonObjectParser(this.jsonFactory); - final GenericUrl sendUrl = new GenericUrl(fcmSendUrl); - for (Message message : messages) { - // Using a separate request factory without authorization is faster for large batches. - // A simple performance test showed a 400-500ms speed up for batches of 1000 messages. - HttpRequest request = childRequestFactory.buildPostRequest( - sendUrl, - new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); - request.setParser(jsonParser); - setCommonFcmHeaders(request.getHeaders()); - batch.queue( - request, MessagingServiceResponse.class, MessagingServiceErrorResponse.class, callback); - } - - return batch; - } - - private void setCommonFcmHeaders(HttpHeaders headers) { - headers.set("X-GOOG-API-FORMAT-VERSION", "2"); - headers.set("X-Firebase-Client", clientVersion); - } - - private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { - MessagingServiceErrorResponse response = new MessagingServiceErrorResponse(); - if (e.getContent() != null) { - try { - JsonParser parser = jsonFactory.createJsonParser(e.getContent()); - parser.parseAndClose(response); - } catch (IOException ignored) { - // ignored - } - } - - return newException(response, e); - } - - private HttpRequestInitializer getBatchRequestInitializer() { - return new HttpRequestInitializer(){ - @Override - public void initialize(HttpRequest request) throws IOException { - requestFactory.getInitializer().initialize(request); - request.setResponseInterceptor(responseInterceptor); - } - }; - } - - private static FirebaseMessagingException newException(MessagingServiceErrorResponse response) { - return newException(response, null); - } - - private static FirebaseMessagingException newException( - MessagingServiceErrorResponse response, @Nullable HttpResponseException e) { - String code = FCM_ERROR_CODES.get(response.getErrorCode()); - if (code == null) { - code = FirebaseMessaging.UNKNOWN_ERROR; - } - - String msg = response.getErrorMessage(); - if (Strings.isNullOrEmpty(msg)) { - if (e != null) { - msg = String.format("Unexpected HTTP response with status: %d; body: %s", - e.getStatusCode(), e.getContent()); - } else { - msg = String.format("Unexpected HTTP response: %s", response.toString()); - } - } - - return new FirebaseMessagingException(code, msg, e); - } - - private static class MessagingBatchCallback - implements BatchCallback { - - private final ImmutableList.Builder responses = ImmutableList.builder(); - - @Override - public void onSuccess( - MessagingServiceResponse response, HttpHeaders responseHeaders) { - responses.add(SendResponse.fromMessageId(response.getMessageId())); - } - - @Override - public void onFailure( - MessagingServiceErrorResponse error, HttpHeaders responseHeaders) { - responses.add(SendResponse.fromException(newException(error))); - } +interface FirebaseMessagingClient { + + /** + * Sends the given message with FCM. + * + * @param message A non-null {@link Message} to be sent. + * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. + * @return A message ID string. + * @throws FirebaseMessagingException If an error occurs while handing the message off to FCM for + * delivery. + */ + String send(Message message, boolean dryRun) throws FirebaseMessagingException; + + /** + * Sends all the messages in the given list with FCM. + * + * @param messages A non-null, non-empty list of messages. + * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. + * @return A {@link BatchResponse} indicating the result of the operation. + * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for + * delivery. + */ + BatchResponse sendAll(List messages, boolean dryRun) throws FirebaseMessagingException; - List getResponses() { - return this.responses.build(); - } - } } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java new file mode 100644 index 000000000..92dfd0c53 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java @@ -0,0 +1,330 @@ +/* + * Copyright 2019 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.googleapis.batch.BatchCallback; +import com.google.api.client.googleapis.batch.BatchRequest; +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.HttpRequestFactory; +import com.google.api.client.http.HttpRequestInitializer; +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.api.client.json.JsonParser; +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.ApiClientUtils; +import com.google.firebase.internal.Nullable; +import com.google.firebase.internal.SdkUtils; +import com.google.firebase.messaging.internal.MessagingServiceErrorResponse; +import com.google.firebase.messaging.internal.MessagingServiceResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * A helper class for interacting with Firebase Cloud Messaging service. + */ +final class FirebaseMessagingClientImpl implements FirebaseMessagingClient { + + private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; + + private static final String FCM_BATCH_URL = "https://fcm.googleapis.com/batch"; + + private static final String API_FORMAT_VERSION_HEADER = "X-GOOG-API-FORMAT-VERSION"; + + private static final String CLIENT_VERSION_HEADER = "X-Firebase-Client"; + + private static final Map FCM_ERROR_CODES = + ImmutableMap.builder() + // FCM v1 canonical error codes + .put("NOT_FOUND", "registration-token-not-registered") + .put("PERMISSION_DENIED", "mismatched-credential") + .put("RESOURCE_EXHAUSTED", "message-rate-exceeded") + .put("UNAUTHENTICATED", "invalid-apns-credentials") + + // FCM v1 new error codes + .put("APNS_AUTH_ERROR", "invalid-apns-credentials") + .put("INTERNAL", FirebaseMessaging.INTERNAL_ERROR) + .put("INVALID_ARGUMENT", "invalid-argument") + .put("QUOTA_EXCEEDED", "message-rate-exceeded") + .put("SENDER_ID_MISMATCH", "mismatched-credential") + .put("UNAVAILABLE", "server-unavailable") + .put("UNREGISTERED", "registration-token-not-registered") + .build(); + + private final String fcmSendUrl; + private final HttpRequestFactory requestFactory; + private final HttpRequestFactory childRequestFactory; + private final JsonFactory jsonFactory; + private final HttpResponseInterceptor responseInterceptor; + private final String clientVersion = "fire-admin-java/" + SdkUtils.getVersion(); + + private FirebaseMessagingClientImpl(Builder builder) { + checkArgument(!Strings.isNullOrEmpty(builder.projectId)); + this.fcmSendUrl = String.format(FCM_URL, builder.projectId); + this.requestFactory = checkNotNull(builder.requestFactory); + this.childRequestFactory = checkNotNull(builder.childRequestFactory); + this.jsonFactory = checkNotNull(builder.jsonFactory); + this.responseInterceptor = builder.responseInterceptor; + } + + @VisibleForTesting + String getFcmSendUrl() { + return fcmSendUrl; + } + + @VisibleForTesting + HttpRequestFactory getRequestFactory() { + return requestFactory; + } + + @VisibleForTesting + HttpRequestFactory getChildRequestFactory() { + return childRequestFactory; + } + + @VisibleForTesting + JsonFactory getJsonFactory() { + return jsonFactory; + } + + @VisibleForTesting + String getClientVersion() { + return clientVersion; + } + + public String send(Message message, boolean dryRun) throws FirebaseMessagingException { + try { + return sendSingleRequest(message, dryRun); + } catch (HttpResponseException e) { + throw createExceptionFromResponse(e); + } catch (IOException e) { + throw new FirebaseMessagingException( + FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); + } + } + + public BatchResponse sendAll( + List messages, boolean dryRun) throws FirebaseMessagingException { + try { + return sendBatchRequest(messages, dryRun); + } catch (HttpResponseException e) { + throw createExceptionFromResponse(e); + } catch (IOException e) { + throw new FirebaseMessagingException( + FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); + } + } + + private String sendSingleRequest(Message message, boolean dryRun) throws IOException { + HttpRequest request = requestFactory.buildPostRequest( + new GenericUrl(fcmSendUrl), + new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); + setCommonFcmHeaders(request.getHeaders()); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(responseInterceptor); + HttpResponse response = request.execute(); + try { + MessagingServiceResponse parsed = new MessagingServiceResponse(); + jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); + return parsed.getMessageId(); + } finally { + ApiClientUtils.disconnectQuietly(response); + } + } + + private BatchResponse sendBatchRequest( + List messages, boolean dryRun) throws IOException { + + MessagingBatchCallback callback = new MessagingBatchCallback(); + BatchRequest batch = newBatchRequest(messages, dryRun, callback); + batch.execute(); + return new BatchResponse(callback.getResponses()); + } + + private BatchRequest newBatchRequest( + List messages, boolean dryRun, MessagingBatchCallback callback) throws IOException { + + BatchRequest batch = new BatchRequest( + requestFactory.getTransport(), getBatchRequestInitializer()); + batch.setBatchUrl(new GenericUrl(FCM_BATCH_URL)); + + final JsonObjectParser jsonParser = new JsonObjectParser(this.jsonFactory); + final GenericUrl sendUrl = new GenericUrl(fcmSendUrl); + for (Message message : messages) { + // Using a separate request factory without authorization is faster for large batches. + // A simple performance test showed a 400-500ms speed up for batches of 1000 messages. + HttpRequest request = childRequestFactory.buildPostRequest( + sendUrl, + new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); + request.setParser(jsonParser); + setCommonFcmHeaders(request.getHeaders()); + batch.queue( + request, MessagingServiceResponse.class, MessagingServiceErrorResponse.class, callback); + } + return batch; + } + + private void setCommonFcmHeaders(HttpHeaders headers) { + headers.set(API_FORMAT_VERSION_HEADER, "2"); + headers.set(CLIENT_VERSION_HEADER, clientVersion); + } + + private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { + MessagingServiceErrorResponse response = new MessagingServiceErrorResponse(); + if (e.getContent() != null) { + try { + JsonParser parser = jsonFactory.createJsonParser(e.getContent()); + parser.parseAndClose(response); + } catch (IOException ignored) { + // ignored + } + } + + return newException(response, e); + } + + private HttpRequestInitializer getBatchRequestInitializer() { + return new HttpRequestInitializer() { + @Override + public void initialize(HttpRequest request) throws IOException { + HttpRequestInitializer initializer = requestFactory.getInitializer(); + if (initializer != null) { + initializer.initialize(request); + } + request.setResponseInterceptor(responseInterceptor); + } + }; + } + + static FirebaseMessagingClientImpl fromApp(FirebaseApp app) { + String projectId = ImplFirebaseTrampolines.getProjectId(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 GOOGLE_CLOUD_PROJECT environment variable."); + return FirebaseMessagingClientImpl.builder() + .setProjectId(projectId) + .setRequestFactory(ApiClientUtils.newAuthorizedRequestFactory(app)) + .setChildRequestFactory(ApiClientUtils.newUnauthorizedRequestFactory(app)) + .setJsonFactory(app.getOptions().getJsonFactory()) + .build(); + } + + static Builder builder() { + return new Builder(); + } + + static final class Builder { + + private String projectId; + private HttpRequestFactory requestFactory; + private HttpRequestFactory childRequestFactory; + private JsonFactory jsonFactory; + private HttpResponseInterceptor responseInterceptor; + + private Builder() { } + + Builder setProjectId(String projectId) { + this.projectId = projectId; + return this; + } + + Builder setRequestFactory(HttpRequestFactory requestFactory) { + this.requestFactory = requestFactory; + return this; + } + + Builder setChildRequestFactory(HttpRequestFactory childRequestFactory) { + this.childRequestFactory = childRequestFactory; + return this; + } + + Builder setJsonFactory(JsonFactory jsonFactory) { + this.jsonFactory = jsonFactory; + return this; + } + + Builder setResponseInterceptor(HttpResponseInterceptor responseInterceptor) { + this.responseInterceptor = responseInterceptor; + return this; + } + + FirebaseMessagingClientImpl build() { + return new FirebaseMessagingClientImpl(this); + } + } + + private static FirebaseMessagingException newException(MessagingServiceErrorResponse response) { + return newException(response, null); + } + + private static FirebaseMessagingException newException( + MessagingServiceErrorResponse response, @Nullable HttpResponseException e) { + String code = FCM_ERROR_CODES.get(response.getErrorCode()); + if (code == null) { + code = FirebaseMessaging.UNKNOWN_ERROR; + } + + String msg = response.getErrorMessage(); + if (Strings.isNullOrEmpty(msg)) { + if (e != null) { + msg = String.format("Unexpected HTTP response with status: %d; body: %s", + e.getStatusCode(), e.getContent()); + } else { + msg = String.format("Unexpected HTTP response: %s", response.toString()); + } + } + + return new FirebaseMessagingException(code, msg, e); + } + + private static class MessagingBatchCallback + implements BatchCallback { + + private final ImmutableList.Builder responses = ImmutableList.builder(); + + @Override + public void onSuccess( + MessagingServiceResponse response, HttpHeaders responseHeaders) { + responses.add(SendResponse.fromMessageId(response.getMessageId())); + } + + @Override + public void onFailure( + MessagingServiceErrorResponse error, HttpHeaders responseHeaders) { + responses.add(SendResponse.fromException(newException(error))); + } + + List getResponses() { + return this.responses.build(); + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/InstanceIdClient.java b/src/main/java/com/google/firebase/messaging/InstanceIdClient.java index 017029094..8c1b70a3b 100644 --- a/src/main/java/com/google/firebase/messaging/InstanceIdClient.java +++ b/src/main/java/com/google/firebase/messaging/InstanceIdClient.java @@ -1,166 +1,30 @@ -/* - * Copyright 2019 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 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.GenericJson; -import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; -import com.google.api.client.json.JsonParser; -import com.google.api.client.util.Key; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; -import com.google.firebase.FirebaseApp; -import com.google.firebase.internal.ApiClientUtils; -import com.google.firebase.internal.Nullable; - -import java.io.IOException; import java.util.List; -import java.util.Map; /** - * A helper class for interacting with the Firebase Instance ID service. Implements the FCM - * topic management functionality. + * An interface for managing FCM topic subscriptions. */ -final class InstanceIdClient { - - private static final String IID_HOST = "https://iid.googleapis.com"; - - private static final String IID_SUBSCRIBE_PATH = "iid/v1:batchAdd"; - - private static final String IID_UNSUBSCRIBE_PATH = "iid/v1:batchRemove"; - - static final Map IID_ERROR_CODES = - ImmutableMap.builder() - .put(400, "invalid-argument") - .put(401, "authentication-error") - .put(403, "authentication-error") - .put(500, FirebaseMessaging.INTERNAL_ERROR) - .put(503, "server-unavailable") - .build(); - - private final HttpRequestFactory requestFactory; - private final JsonFactory jsonFactory; - private final HttpResponseInterceptor responseInterceptor; - - InstanceIdClient(FirebaseApp app, @Nullable HttpResponseInterceptor responseInterceptor) { - this.requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); - this.jsonFactory = app.getOptions().getJsonFactory(); - this.responseInterceptor = responseInterceptor; - } - +interface InstanceIdClient { + + /** + * Subscribes a list of registration tokens to a topic. + * + * @param registrationTokens A non-null, non-empty list of device registration tokens. + * @param topic Name of the topic to subscribe to. May contain the {@code /topics/} prefix. + * @return A {@link TopicManagementResponse}. + */ TopicManagementResponse subscribeToTopic( - String topic, List registrationTokens) throws FirebaseMessagingException { - try { - return sendInstanceIdRequest(topic, registrationTokens, IID_SUBSCRIBE_PATH); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); - } - } - + String topic, List registrationTokens) throws FirebaseMessagingException; + + /** + * Unsubscribes a list of registration tokens from a topic. + * + * @param registrationTokens A non-null, non-empty list of device registration tokens. + * @param topic Name of the topic to unsubscribe from. May contain the {@code /topics/} prefix. + * @return A {@link TopicManagementResponse}. + */ TopicManagementResponse unsubscribeFromTopic( - String topic, List registrationTokens) throws FirebaseMessagingException { - try { - return sendInstanceIdRequest(topic, registrationTokens, IID_UNSUBSCRIBE_PATH); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); - } - } - - private TopicManagementResponse sendInstanceIdRequest( - String topic, List registrationTokens, String path) throws IOException { - String url = String.format("%s/%s", IID_HOST, path); - Map payload = ImmutableMap.of( - "to", getPrefixedTopic(topic), - "registration_tokens", registrationTokens - ); - HttpResponse response = null; - try { - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); - request.getHeaders().set("access_token_auth", "true"); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(responseInterceptor); - response = request.execute(); - - JsonParser parser = jsonFactory.createJsonParser(response.getContent()); - InstanceIdServiceResponse parsedResponse = new InstanceIdServiceResponse(); - parser.parse(parsedResponse); - return new TopicManagementResponse(parsedResponse.results); - } finally { - ApiClientUtils.disconnectQuietly(response); - } - } - - private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { - InstanceIdServiceErrorResponse response = new InstanceIdServiceErrorResponse(); - if (e.getContent() != null) { - try { - JsonParser parser = jsonFactory.createJsonParser(e.getContent()); - parser.parseAndClose(response); - } catch (IOException ignored) { - // ignored - } - } - return newException(response, e); - } - - private String getPrefixedTopic(String topic) { - if (topic.startsWith("/topics/")) { - return topic; - } else { - return "/topics/" + topic; - } - } - - private static FirebaseMessagingException newException( - InstanceIdServiceErrorResponse response, HttpResponseException e) { - // Infer error code from HTTP status - String code = IID_ERROR_CODES.get(e.getStatusCode()); - if (code == null) { - code = FirebaseMessaging.UNKNOWN_ERROR; - } - String msg = response.error; - if (Strings.isNullOrEmpty(msg)) { - msg = String.format("Unexpected HTTP response with status: %d; body: %s", - e.getStatusCode(), e.getContent()); - } - return new FirebaseMessagingException(code, msg, e); - } - - private static class InstanceIdServiceResponse { - @Key("results") - private List results; - } + String topic, List registrationTokens) throws FirebaseMessagingException; - private static class InstanceIdServiceErrorResponse { - @Key("error") - private String error; - } } diff --git a/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java b/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java new file mode 100644 index 000000000..15f8158a5 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java @@ -0,0 +1,192 @@ +/* + * Copyright 2019 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.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.HttpResponseException; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.json.JsonParser; +import com.google.api.client.util.Key; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.FirebaseApp; +import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.Nullable; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * A helper class for interacting with the Firebase Instance ID service. Implements the FCM + * topic management functionality. + */ +final class InstanceIdClientImpl implements InstanceIdClient { + + private static final String IID_HOST = "https://iid.googleapis.com"; + + private static final String IID_SUBSCRIBE_PATH = "iid/v1:batchAdd"; + + private static final String IID_UNSUBSCRIBE_PATH = "iid/v1:batchRemove"; + + static final Map IID_ERROR_CODES = + ImmutableMap.builder() + .put(400, "invalid-argument") + .put(401, "authentication-error") + .put(403, "authentication-error") + .put(500, FirebaseMessaging.INTERNAL_ERROR) + .put(503, "server-unavailable") + .build(); + + private final HttpRequestFactory requestFactory; + private final JsonFactory jsonFactory; + private final HttpResponseInterceptor responseInterceptor; + + InstanceIdClientImpl(HttpRequestFactory requestFactory, JsonFactory jsonFactory) { + this(requestFactory, jsonFactory, null); + } + + InstanceIdClientImpl( + HttpRequestFactory requestFactory, + JsonFactory jsonFactory, + @Nullable HttpResponseInterceptor responseInterceptor) { + this.requestFactory = checkNotNull(requestFactory); + this.jsonFactory = checkNotNull(jsonFactory); + this.responseInterceptor = responseInterceptor; + } + + static InstanceIdClientImpl fromApp(FirebaseApp app) { + return new InstanceIdClientImpl( + ApiClientUtils.newAuthorizedRequestFactory(app), + app.getOptions().getJsonFactory()); + } + + @VisibleForTesting + HttpRequestFactory getRequestFactory() { + return requestFactory; + } + + @VisibleForTesting + JsonFactory getJsonFactory() { + return jsonFactory; + } + + public TopicManagementResponse subscribeToTopic( + String topic, List registrationTokens) throws FirebaseMessagingException { + try { + return sendInstanceIdRequest(topic, registrationTokens, IID_SUBSCRIBE_PATH); + } catch (HttpResponseException e) { + throw createExceptionFromResponse(e); + } catch (IOException e) { + throw new FirebaseMessagingException( + FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); + } + } + + public TopicManagementResponse unsubscribeFromTopic( + String topic, List registrationTokens) throws FirebaseMessagingException { + try { + return sendInstanceIdRequest(topic, registrationTokens, IID_UNSUBSCRIBE_PATH); + } catch (HttpResponseException e) { + throw createExceptionFromResponse(e); + } catch (IOException e) { + throw new FirebaseMessagingException( + FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); + } + } + + private TopicManagementResponse sendInstanceIdRequest( + String topic, List registrationTokens, String path) throws IOException { + String url = String.format("%s/%s", IID_HOST, path); + Map payload = ImmutableMap.of( + "to", getPrefixedTopic(topic), + "registration_tokens", registrationTokens + ); + HttpResponse response = null; + try { + HttpRequest request = requestFactory.buildPostRequest( + new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); + request.getHeaders().set("access_token_auth", "true"); + request.setParser(new JsonObjectParser(jsonFactory)); + request.setResponseInterceptor(responseInterceptor); + response = request.execute(); + + JsonParser parser = jsonFactory.createJsonParser(response.getContent()); + InstanceIdServiceResponse parsedResponse = new InstanceIdServiceResponse(); + parser.parse(parsedResponse); + return new TopicManagementResponse(parsedResponse.results); + } finally { + ApiClientUtils.disconnectQuietly(response); + } + } + + private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { + InstanceIdServiceErrorResponse response = new InstanceIdServiceErrorResponse(); + if (e.getContent() != null) { + try { + JsonParser parser = jsonFactory.createJsonParser(e.getContent()); + parser.parseAndClose(response); + } catch (IOException ignored) { + // ignored + } + } + return newException(response, e); + } + + private String getPrefixedTopic(String topic) { + if (topic.startsWith("/topics/")) { + return topic; + } else { + return "/topics/" + topic; + } + } + + private static FirebaseMessagingException newException( + InstanceIdServiceErrorResponse response, HttpResponseException e) { + // Infer error code from HTTP status + String code = IID_ERROR_CODES.get(e.getStatusCode()); + if (code == null) { + code = FirebaseMessaging.UNKNOWN_ERROR; + } + String msg = response.error; + if (Strings.isNullOrEmpty(msg)) { + msg = String.format("Unexpected HTTP response with status: %d; body: %s", + e.getStatusCode(), e.getContent()); + } + return new FirebaseMessagingException(code, msg, e); + } + + private static class InstanceIdServiceResponse { + @Key("results") + private List results; + } + + private static class InstanceIdServiceErrorResponse { + @Key("error") + private String error; + } +} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java new file mode 100644 index 000000000..87e9499cc --- /dev/null +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -0,0 +1,819 @@ +/* + * 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.http.HttpTransport; +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.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.SdkUtils; +import com.google.firebase.messaging.WebpushNotification.Action; +import com.google.firebase.messaging.WebpushNotification.Direction; +import com.google.firebase.testing.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; + +public class FirebaseMessagingClientImplTest { + + private static final String TEST_FCM_URL = + "https://fcm.googleapis.com/v1/projects/test-project/messages:send"; + + private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); + + private static final String MOCK_RESPONSE = "{\"name\": \"mock-name\"}"; + + private static final String MOCK_BATCH_SUCCESS_RESPONSE = TestUtils.loadResource( + "fcm_batch_success.txt"); + + private static final String MOCK_BATCH_FAILURE_RESPONSE = TestUtils.loadResource( + "fcm_batch_failure.txt"); + + private static final Message EMPTY_MESSAGE = Message.builder() + .setTopic("test-topic") + .build(); + private static final List MESSAGE_LIST = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE); + + private static final boolean DRY_RUN_ENABLED = true; + private static final boolean DRY_RUN_DISABLED = false; + + private MockLowLevelHttpResponse response; + private TestResponseInterceptor interceptor; + private FirebaseMessagingClient client; + + @Before + public void setUp() { + response = new MockLowLevelHttpResponse(); + interceptor = new TestResponseInterceptor(); + client = initMessagingClient(response, interceptor); + } + + @Test + public void testSend() throws Exception { + Map> testMessages = buildTestMessages(); + + for (Map.Entry> entry : testMessages.entrySet()) { + response.setContent(MOCK_RESPONSE); + String resp = client.send(entry.getKey(), DRY_RUN_DISABLED); + + assertEquals("mock-name", resp); + checkRequestHeader(interceptor.getLastRequest()); + checkRequest(interceptor.getLastRequest(), + ImmutableMap.of("message", entry.getValue())); + } + } + + @Test + public void testSendDryRun() throws Exception { + Map> testMessages = buildTestMessages(); + + for (Map.Entry> entry : testMessages.entrySet()) { + response.setContent(MOCK_RESPONSE); + String resp = client.send(entry.getKey(), DRY_RUN_ENABLED); + + assertEquals("mock-name", resp); + checkRequestHeader(interceptor.getLastRequest()); + checkRequest(interceptor.getLastRequest(), + ImmutableMap.of("message", entry.getValue(), "validate_only", true)); + } + } + + @Test + public void testSendHttpError() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent("{}"); + + try { + client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, "unknown-error", + "Unexpected HTTP response with status: " + code + "; body: {}"); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendTransportError() { + client = initClientWithFaultyTransport(); + + try { + client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling FCM backend service", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test + public void testSendSuccessResponseWithUnexpectedPayload() { + Map> testMessages = buildTestMessages(); + + for (Map.Entry> entry : testMessages.entrySet()) { + response.setContent("not valid json"); + + try { + client.send(entry.getKey(), DRY_RUN_DISABLED); + fail("No error thrown for malformed response"); + } catch (FirebaseMessagingException error) { + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling FCM backend service", error.getMessage()); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendErrorWithZeroContentResponse() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setZeroContent(); + + try { + client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, "unknown-error", + "Unexpected HTTP response with status: " + code + "; body: null"); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendErrorWithMalformedResponse() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent("not json"); + + try { + client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, "unknown-error", + "Unexpected HTTP response with status: " + code + "; body: not json"); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendErrorWithDetails() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); + + try { + client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, "invalid-argument"); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendErrorWithCanonicalCode() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}"); + + try { + client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, "registration-token-not-registered"); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendErrorWithFcmError() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " + + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); + + try { + client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, "registration-token-not-registered"); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAll() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient client = initMessagingClientForBatchRequests( + MOCK_BATCH_SUCCESS_RESPONSE, interceptor); + + BatchResponse responses = client.sendAll(MESSAGE_LIST, false); + + assertBatchResponse(responses, interceptor, 2, 0); + } + + @Test + public void testSendAllDryRun() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient client = initMessagingClientForBatchRequests( + MOCK_BATCH_SUCCESS_RESPONSE, interceptor); + + BatchResponse responses = client.sendAll(MESSAGE_LIST, true); + + assertBatchResponse(responses, interceptor, 2, 0); + } + + @Test + public void testRequestInitializerAppliedToBatchRequests() throws Exception { + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(getBatchResponse(MOCK_BATCH_SUCCESS_RESPONSE)) + .build(); + HttpRequestInitializer initializer = new HttpRequestInitializer() { + @Override + public void initialize(HttpRequest httpRequest) { + httpRequest.getHeaders().set("x-custom-header", "test-value"); + } + }; + FirebaseMessagingClientImpl client = FirebaseMessagingClientImpl.builder() + .setProjectId("test-project") + .setJsonFactory(Utils.getDefaultJsonFactory()) + .setRequestFactory(transport.createRequestFactory(initializer)) + .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()) + .setResponseInterceptor(interceptor) + .build(); + + try { + client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); + } finally { + HttpRequest request = interceptor.getLastRequest(); + assertEquals("test-value", request.getHeaders().get("x-custom-header")); + } + } + + @Test + public void testSendAllFailure() throws Exception { + final TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseMessagingClient client = initMessagingClientForBatchRequests( + MOCK_BATCH_FAILURE_RESPONSE, interceptor); + List messages = ImmutableList.of(EMPTY_MESSAGE, EMPTY_MESSAGE, EMPTY_MESSAGE); + + BatchResponse responses = client.sendAll(messages, DRY_RUN_DISABLED); + + assertBatchResponse(responses, interceptor, 1, 2); + } + + @Test + public void testSendAllHttpError() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent("{}"); + + try { + client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, "unknown-error", + "Unexpected HTTP response with status: " + code + "; body: {}"); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllTransportError() { + FirebaseMessagingClient client = initClientWithFaultyTransport(); + + try { + client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling FCM backend service", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test + public void testSendAllErrorWithEmptyResponse() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setZeroContent(); + + try { + client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, "unknown-error", + "Unexpected HTTP response with status: " + code + "; body: null"); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllErrorWithDetails() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); + + try { + client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, "invalid-argument"); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllErrorWithCanonicalCode() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}"); + + try { + client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, "registration-token-not-registered"); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllErrorWithFcmError() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " + + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); + + try { + client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, "registration-token-not-registered"); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendAllErrorWithoutMessage() { + final String responseBody = "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " + + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"; + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent(responseBody); + + try { + client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, "registration-token-not-registered", + "Unexpected HTTP response with status: " + code + "; body: " + responseBody); + } + checkBatchRequestHeader(interceptor.getLastRequest()); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testBuilderNullProjectId() { + fullyPopulatedBuilder().setProjectId(null).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBuilderEmptyProjectId() { + fullyPopulatedBuilder().setProjectId("").build(); + } + + @Test(expected = NullPointerException.class) + public void testBuilderNullRequestFactory() { + fullyPopulatedBuilder().setRequestFactory(null).build(); + } + + @Test(expected = NullPointerException.class) + public void testBuilderNullChildRequestFactory() { + fullyPopulatedBuilder().setChildRequestFactory(null).build(); + } + + @Test + public void testFromApp() throws IOException { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + + try { + FirebaseMessagingClientImpl client = FirebaseMessagingClientImpl.fromApp(app); + + assertEquals(TEST_FCM_URL, client.getFcmSendUrl()); + assertEquals("fire-admin-java/" + SdkUtils.getVersion(), client.getClientVersion()); + assertSame(options.getJsonFactory(), client.getJsonFactory()); + + HttpRequest request = client.getRequestFactory().buildGetRequest( + new GenericUrl("https://example.com")); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + + request = client.getChildRequestFactory().buildGetRequest( + new GenericUrl("https://example.com")); + assertNull(request.getHeaders().getAuthorization()); + } finally { + app.delete(); + } + } + + private FirebaseMessagingClientImpl initMessagingClient( + MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(mockResponse) + .build(); + + return FirebaseMessagingClientImpl.builder() + .setProjectId("test-project") + .setJsonFactory(Utils.getDefaultJsonFactory()) + .setRequestFactory(transport.createRequestFactory()) + .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()) + .setResponseInterceptor(interceptor) + .build(); + } + + private FirebaseMessagingClientImpl initMessagingClientForBatchRequests( + String responsePayload, TestResponseInterceptor interceptor) { + MockLowLevelHttpResponse httpResponse = getBatchResponse(responsePayload); + return initMessagingClient(httpResponse, interceptor); + } + + private MockLowLevelHttpResponse getBatchResponse(String responsePayload) { + return new MockLowLevelHttpResponse() + .setContentType("multipart/mixed; boundary=test_boundary") + .setContent(responsePayload); + } + + private FirebaseMessagingClientImpl initClientWithFaultyTransport() { + HttpTransport transport = TestUtils.createFaultyHttpTransport(); + return FirebaseMessagingClientImpl.builder() + .setProjectId("test-project") + .setJsonFactory(Utils.getDefaultJsonFactory()) + .setRequestFactory(transport.createRequestFactory()) + .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()) + .build(); + } + + private void checkRequestHeader(HttpRequest request) { + assertEquals("POST", request.getRequestMethod()); + assertEquals(TEST_FCM_URL, request.getUrl().toString()); + HttpHeaders headers = request.getHeaders(); + assertEquals("2", headers.get("X-GOOG-API-FORMAT-VERSION")); + assertEquals("fire-admin-java/" + SdkUtils.getVersion(), headers.get("X-Firebase-Client")); + } + + private void checkRequest( + HttpRequest request, Map expected) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + request.getContent().writeTo(out); + JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); + Map parsed = new HashMap<>(); + parser.parseAndClose(parsed); + assertEquals(expected, parsed); + } + + private void assertBatchResponse( + BatchResponse batchResponse, TestResponseInterceptor interceptor, + int successCount, int failureCount) throws IOException { + + assertEquals(successCount, batchResponse.getSuccessCount()); + assertEquals(failureCount, batchResponse.getFailureCount()); + + List responses = batchResponse.getResponses(); + assertEquals(successCount + failureCount, responses.size()); + for (int i = 0; i < successCount; i++) { + SendResponse sendResponse = responses.get(i); + assertTrue(sendResponse.isSuccessful()); + assertEquals("projects/test-project/messages/" + (i + 1), sendResponse.getMessageId()); + assertNull(sendResponse.getException()); + } + + for (int i = successCount; i < failureCount; i++) { + SendResponse sendResponse = responses.get(i); + assertFalse(sendResponse.isSuccessful()); + assertNull(sendResponse.getMessageId()); + + FirebaseMessagingException exception = sendResponse.getException(); + assertNotNull(exception); + assertEquals("invalid-argument", exception.getErrorCode()); + } + + checkBatchRequestHeader(interceptor.getLastRequest()); + checkBatchRequest(interceptor.getLastRequest(), successCount + failureCount); + } + + private void checkBatchRequestHeader(HttpRequest request) { + assertEquals("POST", request.getRequestMethod()); + assertEquals("https://fcm.googleapis.com/batch", request.getUrl().toString()); + } + + private void checkBatchRequest(HttpRequest request, int expectedParts) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + request.getContent().writeTo(out); + String[] lines = out.toString().split("\n"); + assertEquals(expectedParts, countLinesWithPrefix(lines, "POST " + TEST_FCM_URL)); + assertEquals(expectedParts, countLinesWithPrefix(lines, "x-goog-api-format-version: 2")); + assertEquals(expectedParts, countLinesWithPrefix( + lines, "x-firebase-client: fire-admin-java/" + SdkUtils.getVersion())); + } + + private int countLinesWithPrefix(String[] lines, String prefix) { + int matchCount = 0; + for (String line : lines) { + if (line.trim().startsWith(prefix)) { + matchCount++; + } + } + return matchCount; + } + + private FirebaseMessagingClientImpl.Builder fullyPopulatedBuilder() { + return FirebaseMessagingClientImpl.builder() + .setProjectId("test-project") + .setJsonFactory(Utils.getDefaultJsonFactory()) + .setRequestFactory(Utils.getDefaultTransport().createRequestFactory()) + .setChildRequestFactory(Utils.getDefaultTransport().createRequestFactory()); + } + + private void checkExceptionFromHttpResponse( + FirebaseMessagingException error, String expectedCode) { + checkExceptionFromHttpResponse(error, expectedCode, "test error"); + } + + private void checkExceptionFromHttpResponse( + FirebaseMessagingException error, String expectedCode, String expectedMessage) { + assertEquals(expectedCode, error.getErrorCode()); + assertEquals(expectedMessage, error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + private static Map> buildTestMessages() { + // Create a map of FCM messages to their expected JSON serializations. + ImmutableMap.Builder> builder = ImmutableMap.builder(); + + // Empty message + builder.put( + EMPTY_MESSAGE, + ImmutableMap.of("topic", "test-topic")); + + // Notification message + builder.put( + Message.builder() + .setNotification(new Notification("test title", "test body")) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "notification", ImmutableMap.of("title", "test title", "body", "test body"))); + + // Data message + builder.put( + Message.builder() + .putData("k1", "v1") + .putData("k2", "v2") + .putAllData(ImmutableMap.of("k3", "v3", "k4", "v4")) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3", "k4", "v4"))); + + // Android message + builder.put( + Message.builder() + .setAndroidConfig(AndroidConfig.builder() + .setPriority(AndroidConfig.Priority.HIGH) + .setTtl(TimeUnit.SECONDS.toMillis(123)) + .setRestrictedPackageName("test-package") + .setCollapseKey("test-key") + .setNotification(AndroidNotification.builder() + .setClickAction("test-action") + .setTitle("test-title") + .setBody("test-body") + .setIcon("test-icon") + .setColor("#112233") + .setTag("test-tag") + .setSound("test-sound") + .setTitleLocalizationKey("test-title-key") + .setBodyLocalizationKey("test-body-key") + .addTitleLocalizationArg("t-arg1") + .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") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "android", ImmutableMap.of( + "priority", "high", + "collapse_key", "test-key", + "ttl", "123s", + "restricted_package_name", "test-package", + "notification", ImmutableMap.builder() + .put("click_action", "test-action") + .put("title", "test-title") + .put("body", "test-body") + .put("icon", "test-icon") + .put("color", "#112233") + .put("tag", "test-tag") + .put("sound", "test-sound") + .put("title_loc_key", "test-title-key") + .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() + ) + )); + + // APNS message + builder.put( + Message.builder() + .setApnsConfig(ApnsConfig.builder() + .putHeader("h1", "v1") + .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) + .putAllCustomData(ImmutableMap.of("k1", "v1", "k2", true)) + .setAps(Aps.builder() + .setBadge(42) + .setAlert(ApsAlert.builder() + .setTitle("test-title") + .setSubtitle("test-subtitle") + .setBody("test-body") + .build()) + .build()) + .build()) + .setTopic("test-topic") + .build(), + ImmutableMap.of( + "topic", "test-topic", + "apns", ImmutableMap.of( + "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), + "payload", ImmutableMap.of("k1", "v1", "k2", true, + "aps", ImmutableMap.of("badge", new BigDecimal(42), + "alert", ImmutableMap.of( + "title", "test-title", "subtitle", "test-subtitle", + "body", "test-body")))) + )); + + // 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() + .putHeader("h1", "v1") + .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) + .putData("k1", "v1") + .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) + .setNotification(new WebpushNotification("test-title", "test-body", "test-icon")) + .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.of( + "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(); + } +} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 7a7fbb27e..d5f39ba46 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google Inc. + * Copyright 2019 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,65 +18,39 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; 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.HttpHeaders; -import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpResponseException; -import com.google.api.client.http.HttpResponseInterceptor; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.http.LowLevelHttpRequest; -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.json.GenericJson; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; -import com.google.firebase.internal.SdkUtils; -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 com.google.firebase.testing.TestUtils; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.math.BigDecimal; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Test; public class FirebaseMessagingTest { - private static final String TEST_FCM_URL = - "https://fcm.googleapis.com/v1/projects/test-project/messages:send"; - - private static final String TEST_IID_SUBSCRIBE_URL = - "https://iid.googleapis.com/iid/v1:batchAdd"; - - private static final String TEST_IID_UNSUBSCRIBE_URL = - "https://iid.googleapis.com/iid/v1:batchRemove"; - - private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); - - private static final String MOCK_RESPONSE = "{\"name\": \"mock-name\"}"; - - private static final String MOCK_BATCH_SUCCESS_RESPONSE = TestUtils.loadResource( - "fcm_batch_success.txt"); - - private static final String MOCK_BATCH_FAILURE_RESPONSE = TestUtils.loadResource( - "fcm_batch_failure.txt"); + private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + private static final Message EMPTY_MESSAGE = Message.builder() + .setTopic("test-topic") + .build(); + private static final MulticastMessage TEST_MULTICAST_MESSAGE = MulticastMessage.builder() + .addToken("test-fcm-token1") + .addToken("test-fcm-token2") + .build(); + private static final FirebaseMessagingException TEST_EXCEPTION = + new FirebaseMessagingException("TEST_CODE", "Test error message", new Exception()); private static final ImmutableList.Builder TOO_MANY_IDS = ImmutableList.builder(); @@ -96,6 +70,8 @@ public class FirebaseMessagingTest { new TopicMgtArgs(ImmutableList.of("id"), ""), new TopicMgtArgs(ImmutableList.of("id"), "foo*") ); + private static final TopicManagementResponse TOPIC_MGT_RESPONSE = new TopicManagementResponse( + ImmutableList.of(new GenericJson())); @After public void tearDown() { @@ -104,36 +80,52 @@ public void tearDown() { @Test public void testGetInstance() { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - FirebaseApp.initializeApp(options); + FirebaseApp.initializeApp(TEST_OPTIONS); FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + assertSame(messaging, FirebaseMessaging.getInstance()); } @Test public void testGetInstanceByApp() { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - FirebaseApp app = FirebaseApp.initializeApp(options, "custom-app"); + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); FirebaseMessaging messaging = FirebaseMessaging.getInstance(app); + assertSame(messaging, FirebaseMessaging.getInstance(app)); } + @Test + public void testDefaultMessagingClient() { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); + FirebaseMessaging messaging = FirebaseMessaging.getInstance(app); + + FirebaseMessagingClient client = messaging.getMessagingClient(); + + assertTrue(client instanceof FirebaseMessagingClientImpl); + assertSame(client, messaging.getMessagingClient()); + String expectedUrl = "https://fcm.googleapis.com/v1/projects/test-project/messages:send"; + assertEquals(expectedUrl, ((FirebaseMessagingClientImpl) client).getFcmSendUrl()); + } + + @Test + public void testDefaultInstanceIdClient() { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); + FirebaseMessaging messaging = FirebaseMessaging.getInstance(app); + + InstanceIdClient client = messaging.getInstanceIdClient(); + + assertTrue(client instanceof InstanceIdClientImpl); + assertSame(client, messaging.getInstanceIdClient()); + } + @Test public void testPostDeleteApp() { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - FirebaseApp app = FirebaseApp.initializeApp(options, "custom-app"); + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); + app.delete(); + try { FirebaseMessaging.getInstance(app); fail("No error thrown for deleted app"); @@ -143,964 +135,636 @@ public void testPostDeleteApp() { } @Test - public void testNoProjectId() { - FirebaseOptions options = new FirebaseOptions.Builder() + public void testMessagingClientWithoutProjectId() { + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .build(); FirebaseApp.initializeApp(options); + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + try { - FirebaseMessaging.getInstance(); + messaging.getMessagingClient(); fail("No error thrown for missing project ID"); } catch (IllegalArgumentException expected) { - // expected + String message = "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 GOOGLE_CLOUD_PROJECT " + + "environment variable."; + assertEquals(message, expected.getMessage()); } } @Test - public void testSendNullMessage() { - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initDefaultMessaging(interceptor); + public void testInstanceIdClientWithoutProjectId() { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .build(); + FirebaseApp.initializeApp(options); + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + + InstanceIdClient client = messaging.getInstanceIdClient(); + + assertTrue(client instanceof InstanceIdClientImpl); + assertSame(client, messaging.getInstanceIdClient()); + } + + @Test + public void testSendNullMessage() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + try { - messaging.sendAsync(null); + messaging.send(null); fail("No error thrown for null message"); } catch (NullPointerException expected) { // expected } - assertNull(interceptor.getResponse()); + assertNull(client.lastMessage); } @Test - public void testSend() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setContent(MOCK_RESPONSE); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final FirebaseMessaging messaging = initMessaging(response, interceptor); - Map> testMessages = buildTestMessages(); - - List> functions = ImmutableList.of( - new GenericFunction() { - @Override - public String call(Object... args) throws Exception { - return messaging.sendAsync((Message) args[0]).get(); - } - }, - new GenericFunction() { - @Override - public String call(Object... args) throws Exception { - return messaging.send((Message) args[0]); - } - } - ); - for (GenericFunction fn : functions) { - for (Map.Entry> entry : testMessages.entrySet()) { - response.setContent(MOCK_RESPONSE); - String resp = fn.call(entry.getKey()); - assertEquals("mock-name", resp); - - checkRequestHeader(interceptor.getLastRequest()); - checkRequest(interceptor.getLastRequest(), - ImmutableMap.of("message", entry.getValue())); - } - } - } + public void testSend() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - @Test - public void testSendDryRun() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setContent(MOCK_RESPONSE); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final FirebaseMessaging messaging = initMessaging(response, interceptor); - Map> testMessages = buildTestMessages(); - - List> functions = ImmutableList.of( - new GenericFunction() { - @Override - public String call(Object... args) throws Exception { - return messaging.sendAsync((Message) args[0], true).get(); - } - }, - new GenericFunction() { - @Override - public String call(Object... args) throws Exception { - return messaging.send((Message) args[0], true); - } - } - ); - - for (GenericFunction fn : functions) { - for (Map.Entry> entry : testMessages.entrySet()) { - response.setContent(MOCK_RESPONSE); - String resp = fn.call(entry.getKey()); - assertEquals("mock-name", resp); - - checkRequestHeader(interceptor.getLastRequest()); - checkRequest(interceptor.getLastRequest(), - ImmutableMap.of("message", entry.getValue(), "validate_only", true)); - } - } - } + String messageId = messaging.send(EMPTY_MESSAGE); - @Test - public void testSendError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent("{}"); - try { - messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("unknown-error", error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: " + code + "; body: {}", - error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkRequestHeader(interceptor.getLastRequest()); - } + assertEquals("test", messageId); + assertSame(EMPTY_MESSAGE, client.lastMessage); + assertFalse(client.isLastDryRun); } @Test - public void testSendErrorWithZeroContentResponse() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setZeroContent(); - try { - messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("unknown-error", error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: " + code + "; body: null", - error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkRequestHeader(interceptor.getLastRequest()); - } - } + public void testSendDryRun() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - @Test - public void testSendErrorWithDetails() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); - try { - messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("invalid-argument", error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkRequestHeader(interceptor.getLastRequest()); - } - } + String messageId = messaging.send(EMPTY_MESSAGE, true); - @Test - public void testSendErrorWithCanonicalCode() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}"); - try { - messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("registration-token-not-registered", error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkRequestHeader(interceptor.getLastRequest()); - } + assertEquals("test", messageId); + assertSame(EMPTY_MESSAGE, client.lastMessage); + assertTrue(client.isLastDryRun); } @Test - public void testSendErrorWithFcmError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " - + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" - + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); - try { - messaging.sendAsync(Message.builder().setTopic("test-topic").build()).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("registration-token-not-registered", error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkRequestHeader(interceptor.getLastRequest()); - } - } + public void testSendFailure() { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - @Test - public void testSendMulticastWithNull() { - FirebaseMessaging messaging = initDefaultMessaging(); try { - messaging.sendMulticastAsync(null); - fail("No error thrown for null multicast message"); - } catch (NullPointerException expected) { - // expected + messaging.send(EMPTY_MESSAGE); + } catch (FirebaseMessagingException e) { + assertSame(TEST_EXCEPTION, e); } - } - @Test - public void testSendMulticast() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_SUCCESS_RESPONSE, interceptor); - MulticastMessage multicast = MulticastMessage.builder() - .addToken("token1") - .addToken("token2") - .build(); - - BatchResponse responses = messaging.sendMulticast(multicast); - - assertSendBatchSuccess(responses, interceptor); + assertSame(EMPTY_MESSAGE, client.lastMessage); + assertFalse(client.isLastDryRun); } @Test - public void testSendMulticastAsync() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_SUCCESS_RESPONSE, interceptor); - MulticastMessage multicast = MulticastMessage.builder() - .addToken("token1") - .addToken("token2") - .build(); + public void testSendAsync() throws Exception { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - BatchResponse responses = messaging.sendMulticastAsync(multicast).get(); + String messageId = messaging.sendAsync(EMPTY_MESSAGE).get(); - assertSendBatchSuccess(responses, interceptor); + assertEquals("test", messageId); + assertSame(EMPTY_MESSAGE, client.lastMessage); + assertFalse(client.isLastDryRun); } @Test - public void testSendMulticastFailure() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_FAILURE_RESPONSE, interceptor); - MulticastMessage multicast = MulticastMessage.builder() - .addToken("token1") - .addToken("token2") - .addToken("token3") - .build(); + public void testSendAsyncDryRun() throws Exception { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId("test"); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - BatchResponse responses = messaging.sendMulticast(multicast); + String messageId = messaging.sendAsync(EMPTY_MESSAGE, true).get(); - assertSendBatchFailure(responses, interceptor); + assertEquals("test", messageId); + assertSame(EMPTY_MESSAGE, client.lastMessage); + assertTrue(client.isLastDryRun); } @Test - public void testSendMulticastAsyncFailure() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_FAILURE_RESPONSE, interceptor); - MulticastMessage multicast = MulticastMessage.builder() - .addToken("token1") - .addToken("token2") - .addToken("token3") - .build(); + public void testSendAsyncFailure() throws InterruptedException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - BatchResponse responses = messaging.sendMulticastAsync(multicast).get(); + try { + messaging.sendAsync(EMPTY_MESSAGE).get(); + } catch (ExecutionException e) { + assertSame(TEST_EXCEPTION, e.getCause()); + } - assertSendBatchFailure(responses, interceptor); + assertSame(EMPTY_MESSAGE, client.lastMessage); + assertFalse(client.isLastDryRun); } @Test - public void testSendAllWithNull() { - FirebaseMessaging messaging = initDefaultMessaging(); + public void testSendAllWithNull() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + try { - messaging.sendAllAsync(null); + messaging.sendAll(null); fail("No error thrown for null message list"); } catch (NullPointerException expected) { // expected } + + assertNull(client.lastBatch); } @Test - public void testSendAllWithEmptyList() { - FirebaseMessaging messaging = initDefaultMessaging(); + public void testSendAllWithEmptyList() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + try { - messaging.sendAllAsync(ImmutableList.of()); + messaging.sendAll(ImmutableList.of()); fail("No error thrown for empty message list"); } catch (IllegalArgumentException expected) { // expected } + + assertNull(client.lastBatch); } @Test - public void testSendAllWithTooManyMessages() { - FirebaseMessaging messaging = initDefaultMessaging(); + public void testSendAllWithTooManyMessages() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); ImmutableList.Builder listBuilder = ImmutableList.builder(); for (int i = 0; i < 101; i++) { listBuilder.add(Message.builder().setTopic("topic").build()); } + try { - messaging.sendAllAsync(listBuilder.build()); + messaging.sendAll(listBuilder.build(), false); fail("No error thrown for too many messages in the list"); } catch (IllegalArgumentException expected) { // expected } + + assertNull(client.lastBatch); } @Test - public void testSendAll() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_SUCCESS_RESPONSE, interceptor); - List messages = ImmutableList.of( - Message.builder().setTopic("topic1").build(), - Message.builder().setTopic("topic2").build() - ); - - BatchResponse responses = messaging.sendAll(messages); - - assertSendBatchSuccess(responses, interceptor); + public void testSendAll() throws FirebaseMessagingException { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); + + BatchResponse response = messaging.sendAll(messages); + + assertSame(batchResponse, response); + assertSame(messages, client.lastBatch); + assertFalse(client.isLastDryRun); } @Test - public void testSendAllAsync() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_SUCCESS_RESPONSE, interceptor); - List messages = ImmutableList.of( - Message.builder().setTopic("topic1").build(), - Message.builder().setTopic("topic2").build() - ); + public void testSendAllDryRun() throws FirebaseMessagingException { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); - BatchResponse responses = messaging.sendAllAsync(messages).get(); + BatchResponse response = messaging.sendAll(messages, true); - assertSendBatchSuccess(responses, interceptor); + assertSame(batchResponse, response); + assertSame(messages, client.lastBatch); + assertTrue(client.isLastDryRun); } @Test - public void testSendAllFailure() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_FAILURE_RESPONSE, interceptor); - List messages = ImmutableList.of( - Message.builder().setTopic("topic1").build(), - Message.builder().setTopic("topic2").build(), - Message.builder().setTopic("topic3").build() - ); - - BatchResponse responses = messaging.sendAll(messages); - - assertSendBatchFailure(responses, interceptor); + public void testSendAllFailure() { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); + + try { + messaging.sendAll(messages); + } catch (FirebaseMessagingException e) { + assertSame(TEST_EXCEPTION, e); + } + + assertSame(messages, client.lastBatch); + assertFalse(client.isLastDryRun); } @Test - public void testSendAllAsyncFailure() throws Exception { - final TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = getMessagingForBatchRequest( - MOCK_BATCH_FAILURE_RESPONSE, interceptor); - List messages = ImmutableList.of( - Message.builder().setTopic("topic1").build(), - Message.builder().setTopic("topic2").build(), - Message.builder().setTopic("topic3").build() - ); - - BatchResponse responses = messaging.sendAllAsync(messages).get(); - - assertSendBatchFailure(responses, interceptor); + public void testSendAllAsync() throws Exception { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); + + BatchResponse response = messaging.sendAllAsync(messages).get(); + + assertSame(batchResponse, response); + assertSame(messages, client.lastBatch); + assertFalse(client.isLastDryRun); } @Test - public void testSendAllHttpError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent("{}"); - try { - messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("unknown-error", error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: " + code + "; body: {}", - error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } + public void testSendAllAsyncDryRun() throws Exception { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); + + BatchResponse response = messaging.sendAllAsync(messages, true).get(); + + assertSame(batchResponse, response); + assertSame(messages, client.lastBatch); + assertTrue(client.isLastDryRun); } @Test - public void testSendAllTransportError() throws Exception { - FirebaseMessaging messaging = initFaultyTransportMessaging(); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); + public void testSendAllAsyncFailure() throws InterruptedException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + ImmutableList messages = ImmutableList.of(EMPTY_MESSAGE); try { messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling FCM backend service", error.getMessage()); - assertTrue(error.getCause() instanceof IOException); + assertSame(TEST_EXCEPTION, e.getCause()); } + + assertSame(messages, client.lastBatch); + assertFalse(client.isLastDryRun); } @Test - public void testSendAllErrorWithEmptyResponse() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setZeroContent(); + public void testSendMulticastWithNull() throws FirebaseMessagingException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - try { - messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("unknown-error", error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: " + code + "; body: null", - error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkBatchRequestHeader(interceptor.getLastRequest()); + try { + messaging.sendMulticast(null); + fail("No error thrown for null multicast message"); + } catch (NullPointerException expected) { + // expected } + + assertNull(client.lastBatch); } @Test - public void testSendAllErrorWithDetails() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); + public void testSendMulticast() throws FirebaseMessagingException { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - try { - messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("invalid-argument", error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } + BatchResponse response = messaging.sendMulticast(TEST_MULTICAST_MESSAGE); + + assertSame(batchResponse, response); + assertEquals(2, client.lastBatch.size()); + assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); + assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); + assertFalse(client.isLastDryRun); } @Test - public void testSendAllErrorWithCanonicalCode() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"NOT_FOUND\", \"message\": \"test error\"}}"); + public void testSendMulticastDryRun() throws FirebaseMessagingException { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - try { - messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("registration-token-not-registered", error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkBatchRequestHeader(interceptor.getLastRequest()); - } + BatchResponse response = messaging.sendMulticast(TEST_MULTICAST_MESSAGE, true); + + assertSame(batchResponse, response); + assertEquals(2, client.lastBatch.size()); + assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); + assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); + assertTrue(client.isLastDryRun); } @Test - public void testSendAllErrorWithFcmError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " - + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" - + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); + public void testSendMulticastFailure() { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - try { - messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("registration-token-not-registered", error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkBatchRequestHeader(interceptor.getLastRequest()); + try { + messaging.sendMulticast(TEST_MULTICAST_MESSAGE); + } catch (FirebaseMessagingException e) { + assertSame(TEST_EXCEPTION, e); } + + assertEquals(2, client.lastBatch.size()); + assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); + assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); + assertFalse(client.isLastDryRun); } @Test - public void testSendAllErrorWithoutMessage() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - List messages = ImmutableList.of(Message.builder() - .setTopic("test-topic") - .build()); - for (int code : HTTP_ERRORS) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" - + ".v1.FcmError\", \"errorCode\": \"UNREGISTERED\"}]}}"); + public void testSendMulticastAsync() throws Exception { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); - try { - messaging.sendAllAsync(messages).get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("registration-token-not-registered", error.getErrorCode()); - assertTrue(error.getMessage().startsWith("Unexpected HTTP response")); - assertTrue(error.getCause() instanceof HttpResponseException); - } - checkBatchRequestHeader(interceptor.getLastRequest()); + BatchResponse response = messaging.sendMulticastAsync(TEST_MULTICAST_MESSAGE).get(); + + assertSame(batchResponse, response); + assertEquals(2, client.lastBatch.size()); + assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); + assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); + assertFalse(client.isLastDryRun); + } + + @Test + public void testSendMulticastAsyncDryRun() throws Exception { + BatchResponse batchResponse = getBatchResponse("test"); + MockFirebaseMessagingClient client = MockFirebaseMessagingClient + .fromBatchResponse(batchResponse); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + BatchResponse response = messaging.sendMulticastAsync(TEST_MULTICAST_MESSAGE, true).get(); + + assertSame(batchResponse, response); + assertEquals(2, client.lastBatch.size()); + assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); + assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); + assertTrue(client.isLastDryRun); + } + + @Test + public void testSendMulticastAsyncFailure() throws InterruptedException { + MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); + + try { + messaging.sendMulticastAsync(TEST_MULTICAST_MESSAGE).get(); + } catch (ExecutionException e) { + assertSame(TEST_EXCEPTION, e.getCause()); } + + assertEquals(2, client.lastBatch.size()); + assertEquals("test-fcm-token1", client.lastBatch.get(0).getToken()); + assertEquals("test-fcm-token2", client.lastBatch.get(1).getToken()); + assertFalse(client.isLastDryRun); } @Test - public void testInvalidSubscribe() { - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initDefaultMessaging(interceptor); + public void testInvalidSubscribe() throws FirebaseMessagingException { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(null); + FirebaseMessaging messaging = getMessagingForTopicManagement( + Suppliers.ofInstance(client)); for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { try { - messaging.subscribeToTopicAsync(args.registrationTokens, args.topic); + messaging.subscribeToTopic(args.registrationTokens, args.topic); fail("No error thrown for invalid args"); } catch (IllegalArgumentException expected) { // expected } + assertNull(client.lastTopic); + assertNull(client.lastBatch); } - - assertNull(interceptor.getResponse()); } @Test - public void testSubscribe() throws Exception { - final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final FirebaseMessaging messaging = initMessaging(response, interceptor); - - List> functions = ImmutableList.of( - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), - "test-topic").get(); - } - }, - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.subscribeToTopic(ImmutableList.of("id1", "id2"), "test-topic"); - } - }, - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.subscribeToTopic(ImmutableList.of("id1", "id2"), - "/topics/test-topic"); - } - } - ); - - for (GenericFunction fn : functions) { - response.setContent(responseString); - TopicManagementResponse result = fn.call(); - checkTopicManagementRequestHeader( - interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); - checkTopicManagementRequest(interceptor.getLastRequest(), result); - } + public void testSubscribeToTopic() throws FirebaseMessagingException { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); + + TopicManagementResponse got = messaging.subscribeToTopic( + ImmutableList.of("id1", "id2"), "test-topic"); + + assertSame(TOPIC_MGT_RESPONSE, got); } @Test - public void testSubscribeError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int statusCode : HTTP_ERRORS) { - response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); - try { - messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } + public void testSubscribeToTopicFailure() { + MockInstanceIdClient client = MockInstanceIdClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); - checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + try { + messaging.subscribeToTopic(ImmutableList.of("id1", "id2"), "test-topic"); + } catch (FirebaseMessagingException e) { + assertSame(TEST_EXCEPTION, e); } } @Test - public void testSubscribeUnknownError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - response.setStatusCode(500).setContent("{}"); - try { - messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: 500; body: {}", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } + public void testSubscribeToTopicAsync() throws Exception { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); - checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + TopicManagementResponse got = messaging.subscribeToTopicAsync( + ImmutableList.of("id1", "id2"), "test-topic").get(); + + assertSame(TOPIC_MGT_RESPONSE, got); } @Test - public void testSubscribeTransportError() throws Exception { - FirebaseMessaging messaging = initFaultyTransportMessaging(); + public void testSubscribeToTopicAsyncFailure() throws InterruptedException { + MockInstanceIdClient client = MockInstanceIdClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); + try { messaging.subscribeToTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling IID backend service", error.getMessage()); - assertTrue(error.getCause() instanceof IOException); + assertSame(TEST_EXCEPTION, e.getCause()); } } @Test - public void testInvalidUnsubscribe() { - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initDefaultMessaging(interceptor); + public void testInvalidUnsubscribe() throws FirebaseMessagingException { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(null); + FirebaseMessaging messaging = getMessagingForTopicManagement( + Suppliers.ofInstance(client)); for (TopicMgtArgs args : INVALID_TOPIC_MGT_ARGS) { try { - messaging.unsubscribeFromTopicAsync(args.registrationTokens, args.topic); + messaging.unsubscribeFromTopic(args.registrationTokens, args.topic); fail("No error thrown for invalid args"); } catch (IllegalArgumentException expected) { // expected } + assertNull(client.lastTopic); + assertNull(client.lastBatch); } - - assertNull(interceptor.getResponse()); } @Test - public void testUnsubscribe() throws Exception { - final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - final FirebaseMessaging messaging = initMessaging(response, interceptor); - - List> functions = ImmutableList.of( - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), - "test-topic").get(); - } - }, - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.unsubscribeFromTopic(ImmutableList.of("id1", "id2"), "test-topic"); - } - }, - new GenericFunction() { - @Override - public TopicManagementResponse call(Object... args) throws Exception { - return messaging.unsubscribeFromTopic(ImmutableList.of("id1", "id2"), - "/topics/test-topic"); - } - } - ); - - for (GenericFunction fn : functions) { - response.setContent(responseString); - TopicManagementResponse result = fn.call(); - checkTopicManagementRequestHeader( - interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); - checkTopicManagementRequest(interceptor.getLastRequest(), result); - } + public void testUnsubscribeFromTopic() throws FirebaseMessagingException { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); + + TopicManagementResponse got = messaging.unsubscribeFromTopic( + ImmutableList.of("id1", "id2"), "test-topic"); + + assertSame(TOPIC_MGT_RESPONSE, got); } @Test - public void testUnsubscribeError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - for (int statusCode : HTTP_ERRORS) { - response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); - try { - messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals(getTopicManagementErrorCode(statusCode), error.getErrorCode()); - assertEquals("test error", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } + public void testUnsubscribeFromTopicFailure() { + MockInstanceIdClient client = MockInstanceIdClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); - checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + try { + messaging.unsubscribeFromTopic(ImmutableList.of("id1", "id2"), "test-topic"); + } catch (FirebaseMessagingException e) { + assertSame(TEST_EXCEPTION, e); } } @Test - public void testUnsubscribeUnknownError() throws Exception { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseMessaging messaging = initMessaging(response, interceptor); - response.setStatusCode(500).setContent("{}"); - try { - messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals(getTopicManagementErrorCode(500), error.getErrorCode()); - assertEquals("Unexpected HTTP response with status: 500; body: {}", error.getMessage()); - assertTrue(error.getCause() instanceof HttpResponseException); - } + public void testUnsubscribeFromTopicAsync() throws Exception { + MockInstanceIdClient client = MockInstanceIdClient.fromResponse(TOPIC_MGT_RESPONSE); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); - checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + TopicManagementResponse got = messaging.unsubscribeFromTopicAsync( + ImmutableList.of("id1", "id2"), "test-topic").get(); + + assertSame(TOPIC_MGT_RESPONSE, got); } @Test - public void testUnsubscribeTransportError() throws Exception { - FirebaseMessaging messaging = initFaultyTransportMessaging(); + public void testUnsubscribeFromTopicAsyncFailure() throws InterruptedException { + MockInstanceIdClient client = MockInstanceIdClient.fromException(TEST_EXCEPTION); + FirebaseMessaging messaging = getMessagingForTopicManagement(Suppliers.ofInstance(client)); + try { messaging.unsubscribeFromTopicAsync(ImmutableList.of("id1", "id2"), "test-topic").get(); - fail("No error thrown for HTTP error"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseMessagingException); - FirebaseMessagingException error = (FirebaseMessagingException) e.getCause(); - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling IID backend service", error.getMessage()); - assertTrue(error.getCause() instanceof IOException); + assertSame(TEST_EXCEPTION, e.getCause()); } } - private static FirebaseMessaging initMessaging( - MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) { - MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(mockResponse) - .build(); - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .setHttpTransport(transport) + private FirebaseMessaging getMessagingForSend( + Supplier supplier) { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); + return FirebaseMessaging.builder() + .setFirebaseApp(app) + .setMessagingClient(supplier) + .setInstanceIdClient(Suppliers.ofInstance(null)) .build(); - FirebaseApp app = FirebaseApp.initializeApp(options); - - return new FirebaseMessaging(app, interceptor); - } - - private static FirebaseMessaging initDefaultMessaging() { - return initDefaultMessaging(null); } - private static FirebaseMessaging initDefaultMessaging(HttpResponseInterceptor interceptor) { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") + private FirebaseMessaging getMessagingForTopicManagement( + Supplier supplier) { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); + return FirebaseMessaging.builder() + .setFirebaseApp(app) + .setMessagingClient(Suppliers.ofInstance(null)) + .setInstanceIdClient(supplier) .build(); - FirebaseApp app = FirebaseApp.initializeApp(options); - return new FirebaseMessaging(app, interceptor); } - private static FirebaseMessaging initFaultyTransportMessaging() { - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .setHttpTransport(new FailingHttpTransport()) - .build(); - FirebaseApp app = FirebaseApp.initializeApp(options); - return new FirebaseMessaging(app, null); + private BatchResponse getBatchResponse(String ...messageIds) { + ImmutableList.Builder listBuilder = ImmutableList.builder(); + for (String messageId : messageIds) { + listBuilder.add(SendResponse.fromMessageId(messageId)); + } + return new BatchResponse(listBuilder.build()); } - private void checkRequest( - HttpRequest request, Map expected) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - request.getContent().writeTo(out); - JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); - Map parsed = new HashMap<>(); - parser.parseAndClose(parsed); - assertEquals(expected, parsed); - } + private static class MockFirebaseMessagingClient implements FirebaseMessagingClient { - private void checkRequestHeader(HttpRequest request) { - assertEquals("POST", request.getRequestMethod()); - assertEquals(TEST_FCM_URL, request.getUrl().toString()); - HttpHeaders headers = request.getHeaders(); - assertEquals("Bearer test-token", headers.getAuthorization()); - assertEquals("2", headers.get("X-GOOG-API-FORMAT-VERSION")); - assertEquals("fire-admin-java/" + SdkUtils.getVersion(), headers.get("X-Firebase-Client")); - } + private String messageId; + private BatchResponse batchResponse; + private FirebaseMessagingException exception; - private FirebaseMessaging getMessagingForBatchRequest( - String responsePayload, TestResponseInterceptor interceptor) { - MockLowLevelHttpResponse httpResponse = new MockLowLevelHttpResponse() - .setContentType("multipart/mixed; boundary=test_boundary") - .setContent(responsePayload); - return initMessaging(httpResponse, interceptor); - } + private Message lastMessage; + private List lastBatch; + private boolean isLastDryRun; - private void assertSendBatchSuccess( - BatchResponse batchResponse, TestResponseInterceptor interceptor) throws IOException { - - assertEquals(2, batchResponse.getSuccessCount()); - assertEquals(0, batchResponse.getFailureCount()); + private MockFirebaseMessagingClient( + String messageId, BatchResponse batchResponse, FirebaseMessagingException exception) { + this.messageId = messageId; + this.batchResponse = batchResponse; + this.exception = exception; + } - List responses = batchResponse.getResponses(); - assertEquals(2, responses.size()); - for (int i = 0; i < 2; i++) { - SendResponse sendResponse = responses.get(i); - assertTrue(sendResponse.isSuccessful()); - assertEquals("projects/test-project/messages/" + (i + 1), sendResponse.getMessageId()); - assertNull(sendResponse.getException()); + static MockFirebaseMessagingClient fromMessageId(String messageId) { + return new MockFirebaseMessagingClient(messageId, null, null); } - checkBatchRequestHeader(interceptor.getLastRequest()); - checkBatchRequest(interceptor.getLastRequest(), 2); - } - private void assertSendBatchFailure( - BatchResponse batchResponse, TestResponseInterceptor interceptor) throws IOException { - - assertEquals(1, batchResponse.getSuccessCount()); - assertEquals(2, batchResponse.getFailureCount()); - - List responses = batchResponse.getResponses(); - assertEquals(3, responses.size()); - SendResponse firstResponse = responses.get(0); - assertTrue(firstResponse.isSuccessful()); - assertEquals("projects/test-project/messages/1", firstResponse.getMessageId()); - assertNull(firstResponse.getException()); - - SendResponse secondResponse = responses.get(1); - assertFalse(secondResponse.isSuccessful()); - assertNull(secondResponse.getMessageId()); - FirebaseMessagingException exception = secondResponse.getException(); - assertNotNull(exception); - assertEquals("invalid-argument", exception.getErrorCode()); - - SendResponse thirdResponse = responses.get(2); - assertFalse(thirdResponse.isSuccessful()); - assertNull(thirdResponse.getMessageId()); - exception = thirdResponse.getException(); - assertNotNull(exception); - assertEquals("invalid-argument", exception.getErrorCode()); - - checkBatchRequestHeader(interceptor.getLastRequest()); - checkBatchRequest(interceptor.getLastRequest(), 3); - } + static MockFirebaseMessagingClient fromBatchResponse(BatchResponse batchResponse) { + return new MockFirebaseMessagingClient(null, batchResponse, null); + } - private void checkBatchRequestHeader(HttpRequest request) { - assertEquals("POST", request.getRequestMethod()); - assertEquals("https://fcm.googleapis.com/batch", request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); - } + static MockFirebaseMessagingClient fromException(FirebaseMessagingException exception) { + return new MockFirebaseMessagingClient(null, null, exception); + } - private void checkBatchRequest(HttpRequest request, int expectedParts) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - request.getContent().writeTo(out); - String[] lines = out.toString().split("\n"); - assertEquals(expectedParts, countLinesWithPrefix(lines, "POST " + TEST_FCM_URL)); - assertEquals(expectedParts, countLinesWithPrefix(lines, "x-goog-api-format-version: 2")); - assertEquals(expectedParts, countLinesWithPrefix( - lines, "x-firebase-client: fire-admin-java/" + SdkUtils.getVersion())); - } + @Override + public String send(Message message, boolean dryRun) throws FirebaseMessagingException { + lastMessage = message; + isLastDryRun = dryRun; + if (exception != null) { + throw exception; + } + return messageId; + } - private int countLinesWithPrefix(String[] lines, String prefix) { - int matchCount = 0; - for (String line : lines) { - if (line.trim().startsWith(prefix)) { - matchCount++; + @Override + public BatchResponse sendAll( + List messages, boolean dryRun) throws FirebaseMessagingException { + lastBatch = messages; + isLastDryRun = dryRun; + if (exception != null) { + throw exception; } + return batchResponse; } - return matchCount; } - private static String getTopicManagementErrorCode(int statusCode) { - String code = InstanceIdClient.IID_ERROR_CODES.get(statusCode); - if (code == null) { - code = "unknown-error"; + private static class MockInstanceIdClient implements InstanceIdClient { + + private TopicManagementResponse response; + private FirebaseMessagingException exception; + + private String lastTopic; + private List lastBatch; + + private MockInstanceIdClient( + TopicManagementResponse response, FirebaseMessagingException exception) { + this.response = response; + this.exception = exception; } - return code; - } + static MockInstanceIdClient fromResponse(TopicManagementResponse response) { + return new MockInstanceIdClient(response, null); + } - private void checkTopicManagementRequest( - HttpRequest request, TopicManagementResponse result) throws IOException { - assertEquals(1, result.getSuccessCount()); - assertEquals(1, result.getFailureCount()); - assertEquals(1, result.getErrors().size()); - assertEquals(1, result.getErrors().get(0).getIndex()); - assertEquals("unknown-error", result.getErrors().get(0).getReason()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - request.getContent().writeTo(out); - Map parsed = new HashMap<>(); - JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); - parser.parseAndClose(parsed); - assertEquals(2, parsed.size()); - assertEquals("/topics/test-topic", parsed.get("to")); - assertEquals(ImmutableList.of("id1", "id2"), parsed.get("registration_tokens")); - } + static MockInstanceIdClient fromException(FirebaseMessagingException exception) { + return new MockInstanceIdClient(null, exception); + } - private void checkTopicManagementRequestHeader( - HttpRequest request, String expectedUrl) { - assertEquals("POST", request.getRequestMethod()); - assertEquals(expectedUrl, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + @Override + public TopicManagementResponse subscribeToTopic( + String topic, List registrationTokens) throws FirebaseMessagingException { + this.lastTopic = topic; + this.lastBatch = registrationTokens; + if (exception != null) { + throw exception; + } + return response; + } + + @Override + public TopicManagementResponse unsubscribeFromTopic( + String topic, List registrationTokens) throws FirebaseMessagingException { + this.lastTopic = topic; + this.lastBatch = registrationTokens; + if (exception != null) { + throw exception; + } + return response; + } } private static class TopicMgtArgs { @@ -1112,224 +776,4 @@ private static class TopicMgtArgs { this.topic = topic; } } - - private static class FailingHttpTransport extends HttpTransport { - @Override - protected LowLevelHttpRequest buildRequest(String method, String url) throws IOException { - throw new IOException("transport error"); - } - } - - private static Map> buildTestMessages() { - ImmutableMap.Builder> builder = ImmutableMap.builder(); - - // Empty message - builder.put( - Message.builder().setTopic("test-topic").build(), - ImmutableMap.of("topic", "test-topic")); - - // Notification message - builder.put( - Message.builder() - .setNotification(new Notification("test title", "test body")) - .setTopic("test-topic") - .build(), - ImmutableMap.of( - "topic", "test-topic", - "notification", ImmutableMap.of("title", "test title", "body", "test body"))); - - // Data message - builder.put( - Message.builder() - .putData("k1", "v1") - .putData("k2", "v2") - .putAllData(ImmutableMap.of("k3", "v3", "k4", "v4")) - .setTopic("test-topic") - .build(), - ImmutableMap.of( - "topic", "test-topic", - "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3", "k4", "v4"))); - - // Android message - builder.put( - Message.builder() - .setAndroidConfig(AndroidConfig.builder() - .setPriority(AndroidConfig.Priority.HIGH) - .setTtl(TimeUnit.SECONDS.toMillis(123)) - .setRestrictedPackageName("test-package") - .setCollapseKey("test-key") - .setNotification(AndroidNotification.builder() - .setClickAction("test-action") - .setTitle("test-title") - .setBody("test-body") - .setIcon("test-icon") - .setColor("#112233") - .setTag("test-tag") - .setSound("test-sound") - .setTitleLocalizationKey("test-title-key") - .setBodyLocalizationKey("test-body-key") - .addTitleLocalizationArg("t-arg1") - .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") - .build(), - ImmutableMap.of( - "topic", "test-topic", - "android", ImmutableMap.of( - "priority", "high", - "collapse_key", "test-key", - "ttl", "123s", - "restricted_package_name", "test-package", - "notification", ImmutableMap.builder() - .put("click_action", "test-action") - .put("title", "test-title") - .put("body", "test-body") - .put("icon", "test-icon") - .put("color", "#112233") - .put("tag", "test-tag") - .put("sound", "test-sound") - .put("title_loc_key", "test-title-key") - .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() - ) - )); - - // APNS message - builder.put( - Message.builder() - .setApnsConfig(ApnsConfig.builder() - .putHeader("h1", "v1") - .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) - .putAllCustomData(ImmutableMap.of("k1", "v1", "k2", true)) - .setAps(Aps.builder() - .setBadge(42) - .setAlert(ApsAlert.builder() - .setTitle("test-title") - .setSubtitle("test-subtitle") - .setBody("test-body") - .build()) - .build()) - .build()) - .setTopic("test-topic") - .build(), - ImmutableMap.of( - "topic", "test-topic", - "apns", ImmutableMap.of( - "headers", ImmutableMap.of("h1", "v1", "h2", "v2", "h3", "v3"), - "payload", ImmutableMap.of("k1", "v1", "k2", true, - "aps", ImmutableMap.of("badge", new BigDecimal(42), - "alert", ImmutableMap.of( - "title", "test-title", "subtitle", "test-subtitle", - "body", "test-body")))) - )); - - // 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() - .putHeader("h1", "v1") - .putAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")) - .putData("k1", "v1") - .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) - .setNotification(new WebpushNotification("test-title", "test-body", "test-icon")) - .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.of( - "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(); - } } diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java new file mode 100644 index 000000000..f939c8382 --- /dev/null +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java @@ -0,0 +1,400 @@ +/* + * Copyright 2019 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.json.GenericJson; +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.common.collect.ImmutableList; +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.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.After; +import org.junit.Test; + +public class InstanceIdClientImplTest { + + private static final String TEST_IID_SUBSCRIBE_URL = + "https://iid.googleapis.com/iid/v1:batchAdd"; + + private static final String TEST_IID_UNSUBSCRIBE_URL = + "https://iid.googleapis.com/iid/v1:batchRemove"; + + private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testSubscribe() throws Exception { + final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(responseString); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final InstanceIdClient client = initInstanceIdClient(response, interceptor); + + TopicManagementResponse result = client.subscribeToTopic( + "test-topic", ImmutableList.of("id1", "id2")); + + checkTopicManagementRequestHeader( + interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + checkTopicManagementRequest(interceptor.getLastRequest(), result); + } + + @Test + public void testSubscribeWithPrefixedTopic() throws Exception { + final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(responseString); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final InstanceIdClient client = initInstanceIdClient(response, interceptor); + + TopicManagementResponse result = client.subscribeToTopic( + "/topics/test-topic", ImmutableList.of("id1", "id2")); + + checkTopicManagementRequestHeader( + interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + checkTopicManagementRequest(interceptor.getLastRequest(), result); + } + + @Test + public void testSubscribeError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + + for (int statusCode : HTTP_ERRORS) { + response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); + + try { + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, statusCode, "test error"); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + } + } + + @Test + public void testSubscribeEmptyPayloadError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(500).setContent("{}"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + + try { + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, 500, + "Unexpected HTTP response with status: 500; body: {}"); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + } + + @Test + public void testSubscribeMalformedError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(500).setContent("not json"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + + try { + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, 500, + "Unexpected HTTP response with status: 500; body: not json"); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + } + + @Test + public void testSubscribeZeroContentError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(500).setZeroContent(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + + try { + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, 500, + "Unexpected HTTP response with status: 500; body: null"); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); + } + + @Test + public void testSubscribeTransportError() { + InstanceIdClient client = initClientWithFaultyTransport(); + + try { + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling IID backend service", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test + public void testUnsubscribe() throws Exception { + final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(responseString); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final InstanceIdClient client = initInstanceIdClient(response, interceptor); + + TopicManagementResponse result = client.unsubscribeFromTopic( + "test-topic", ImmutableList.of("id1", "id2")); + + checkTopicManagementRequestHeader( + interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + checkTopicManagementRequest(interceptor.getLastRequest(), result); + } + + @Test + public void testUnsubscribeWithPrefixedTopic() throws Exception { + final String responseString = "{\"results\": [{}, {\"error\": \"error_reason\"}]}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(responseString); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + final InstanceIdClient client = initInstanceIdClient(response, interceptor); + + TopicManagementResponse result = client.unsubscribeFromTopic( + "/topics/test-topic", ImmutableList.of("id1", "id2")); + + checkTopicManagementRequestHeader( + interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + checkTopicManagementRequest(interceptor.getLastRequest(), result); + } + + @Test + public void testUnsubscribeError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + + for (int statusCode : HTTP_ERRORS) { + response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); + + try { + client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, statusCode, "test error"); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + } + } + + @Test + public void testUnsubscribeEmptyPayloadError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(500).setContent("{}"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + + try { + client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, 500, + "Unexpected HTTP response with status: 500; body: {}"); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + } + + @Test + public void testUnsubscribeMalformedError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(500).setContent("not json"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + + try { + client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, 500, + "Unexpected HTTP response with status: 500; body: not json"); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + } + + @Test + public void testUnsubscribeZeroContentError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(500).setZeroContent(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + + try { + client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, 500, + "Unexpected HTTP response with status: 500; body: null"); + } + + checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); + } + + @Test + public void testUnsubscribeTransportError() { + InstanceIdClient client = initClientWithFaultyTransport(); + + try { + client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals("internal-error", error.getErrorCode()); + assertEquals("Error while calling IID backend service", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test(expected = NullPointerException.class) + public void testRequestFactoryIsNull() { + new InstanceIdClientImpl(null, Utils.getDefaultJsonFactory()); + } + + @Test(expected = NullPointerException.class) + public void testJsonFactoryIsNull() { + new InstanceIdClientImpl(Utils.getDefaultTransport().createRequestFactory(), null); + } + + @Test + public void testFromApp() throws IOException { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + + try { + InstanceIdClientImpl client = InstanceIdClientImpl.fromApp(app); + + assertSame(options.getJsonFactory(), client.getJsonFactory()); + HttpRequest request = client.getRequestFactory().buildGetRequest( + new GenericUrl("https://example.com")); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + } finally { + app.delete(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testTopicManagementResponseWithNullList() { + new TopicManagementResponse(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testTopicManagementResponseWithEmptyList() { + new TopicManagementResponse(ImmutableList.of()); + } + + private static InstanceIdClientImpl initInstanceIdClient( + final MockLowLevelHttpResponse mockResponse, + final HttpResponseInterceptor interceptor) { + + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(mockResponse) + .build(); + return new InstanceIdClientImpl( + transport.createRequestFactory(), + Utils.getDefaultJsonFactory(), + interceptor); + } + + private void checkTopicManagementRequest( + HttpRequest request, TopicManagementResponse result) throws IOException { + assertEquals(1, result.getSuccessCount()); + assertEquals(1, result.getFailureCount()); + assertEquals(1, result.getErrors().size()); + assertEquals(1, result.getErrors().get(0).getIndex()); + assertEquals("unknown-error", result.getErrors().get(0).getReason()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + request.getContent().writeTo(out); + Map parsed = new HashMap<>(); + JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(out.toString()); + parser.parseAndClose(parsed); + assertEquals(2, parsed.size()); + assertEquals("/topics/test-topic", parsed.get("to")); + assertEquals(ImmutableList.of("id1", "id2"), parsed.get("registration_tokens")); + } + + private void checkTopicManagementRequestHeader( + HttpRequest request, String expectedUrl) { + assertEquals("POST", request.getRequestMethod()); + assertEquals(expectedUrl, request.getUrl().toString()); + } + + private void checkExceptionFromHttpResponse(FirebaseMessagingException error, + int expectedCode, String expectedMessage) { + assertEquals(getTopicManagementErrorCode(expectedCode), error.getErrorCode()); + assertEquals(expectedMessage, error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + } + + private InstanceIdClient initClientWithFaultyTransport() { + return new InstanceIdClientImpl( + TestUtils.createFaultyHttpTransport().createRequestFactory(), + Utils.getDefaultJsonFactory()); + } + + private String getTopicManagementErrorCode(int statusCode) { + String code = InstanceIdClientImpl.IID_ERROR_CODES.get(statusCode); + if (code == null) { + code = "unknown-error"; + } + return code; + } +} diff --git a/src/test/java/com/google/firebase/testing/TestUtils.java b/src/test/java/com/google/firebase/testing/TestUtils.java index 3e33a52ea..f8e1b4d0e 100644 --- a/src/test/java/com/google/firebase/testing/TestUtils.java +++ b/src/test/java/com/google/firebase/testing/TestUtils.java @@ -24,6 +24,7 @@ import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpRequest; import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpRequest; @@ -142,4 +143,13 @@ public static HttpRequest createRequest(MockLowLevelHttpRequest request) throws HttpRequestFactory requestFactory = transport.createRequestFactory(); return requestFactory.buildPostRequest(TEST_URL, new EmptyContent()); } + + public static HttpTransport createFaultyHttpTransport() { + return new HttpTransport() { + @Override + protected LowLevelHttpRequest buildRequest(String s, String s1) throws IOException { + throw new IOException("transport error"); + } + }; + } } From 1aa7673afca5bad4309e375cd76158ca92646824 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 19 Apr 2019 10:49:12 -0700 Subject: [PATCH 062/456] Enabling automatic retries for FCM API Calls (#266) * Introduced FirebaseCloudMessaging interface * Improving FCM test coverage * More unit tests * Made InstanceIdClient into an interface * Complete test coverage for FCM * Cleaned up the tests * Fixing a failing test * Enabled HTTP retries for FCM * Updated changelog --- CHANGELOG.md | 3 +- .../firebase/internal/ApiClientUtils.java | 17 ++- .../firebase/internal/ApiClientUtilsTest.java | 118 ++++++++++++++++++ 3 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index cea3b25ec..7417de125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased -- +- [fixed] Enabled automatic retries for FCM API calls failing with + HTTP 500 and 503 errors. # v6.8.0 diff --git a/src/main/java/com/google/firebase/internal/ApiClientUtils.java b/src/main/java/com/google/firebase/internal/ApiClientUtils.java index 62e4320e4..7506ff8ee 100644 --- a/src/main/java/com/google/firebase/internal/ApiClientUtils.java +++ b/src/main/java/com/google/firebase/internal/ApiClientUtils.java @@ -19,6 +19,7 @@ import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpTransport; +import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import java.io.IOException; @@ -28,9 +29,23 @@ */ public class ApiClientUtils { + private static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.builder() + .setMaxRetries(4) + .setRetryStatusCodes(ImmutableList.of(500, 503)) + .setMaxIntervalMillis(60 * 1000) + .build(); + + /** + * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and + * automatic retries. + * + * @param app {@link FirebaseApp} from which to obtain authorization credentials. + * @return A new {@code HttpRequestFactory} instance. + */ public static HttpRequestFactory newAuthorizedRequestFactory(FirebaseApp app) { HttpTransport transport = app.getOptions().getHttpTransport(); - return transport.createRequestFactory(new FirebaseRequestInitializer(app)); + return transport.createRequestFactory( + new FirebaseRequestInitializer(app, DEFAULT_RETRY_CONFIG)); } public static HttpRequestFactory newUnauthorizedRequestFactory(FirebaseApp app) { diff --git a/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java new file mode 100644 index 000000000..78eabaf32 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +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.HttpUnsuccessfulResponseHandler; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.common.collect.ImmutableList; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.RetryInitializer.RetryHandlerDecorator; +import java.io.IOException; +import org.junit.After; +import org.junit.Test; + +public class ApiClientUtilsTest { + + private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .build(); + private static final GenericUrl TEST_URL = new GenericUrl("https://firebase.google.com"); + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testAuthorizedHttpClient() throws IOException { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); + + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + + assertTrue(requestFactory.getInitializer() instanceof FirebaseRequestInitializer); + HttpRequest request = requestFactory.buildGetRequest(TEST_URL); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + assertTrue(retryHandler instanceof RetryHandlerDecorator); + RetryConfig retryConfig = ((RetryHandlerDecorator) retryHandler).getRetryHandler() + .getRetryConfig(); + assertEquals(4, retryConfig.getMaxRetries()); + assertEquals(60 * 1000, retryConfig.getMaxIntervalMillis()); + assertFalse(retryConfig.isRetryOnIOExceptions()); + assertEquals(retryConfig.getRetryStatusCodes(), ImmutableList.of(500, 503)); + } + + @Test + public void testUnauthorizedHttpClient() throws IOException { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); + + HttpRequestFactory requestFactory = ApiClientUtils.newUnauthorizedRequestFactory(app); + + assertNull(requestFactory.getInitializer()); + HttpRequest request = requestFactory.buildGetRequest(TEST_URL); + assertNull(request.getHeaders().getAuthorization()); + HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + assertNull(retryHandler); + } + + @Test + public void testDisconnect() throws IOException { + MockLowLevelHttpResponse lowLevelResponse = new MockLowLevelHttpResponse(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(lowLevelResponse) + .build(); + HttpResponse response = transport.createRequestFactory().buildGetRequest(TEST_URL).execute(); + assertFalse(lowLevelResponse.isDisconnected()); + + ApiClientUtils.disconnectQuietly(response); + + assertTrue(lowLevelResponse.isDisconnected()); + } + + @Test + public void testDisconnectWithErrorSuppression() throws IOException { + MockLowLevelHttpResponse lowLevelResponse = new MockLowLevelHttpResponse(){ + @Override + public void disconnect() throws IOException { + super.disconnect(); + throw new IOException("test error"); + } + }; + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(lowLevelResponse) + .build(); + HttpResponse response = transport.createRequestFactory().buildGetRequest(TEST_URL).execute(); + assertFalse(lowLevelResponse.isDisconnected()); + + ApiClientUtils.disconnectQuietly(response); + + assertTrue(lowLevelResponse.isDisconnected()); + } +} From 55d403f20aabe4deb58e6b4dfc2592e6589f7339 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 25 Apr 2019 14:19:13 -0700 Subject: [PATCH 063/456] Staging release 6.8.1 (#267) * Updating CHANGELOG for 6.8.1 release. * [maven-release-plugin] prepare release v6.8.1 * [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 7417de125..40c9c46aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +- + +# v6.8.1 + - [fixed] Enabled automatic retries for FCM API calls failing with HTTP 500 and 503 errors. diff --git a/pom.xml b/pom.xml index 27e137a8a..284e503ac 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.8.1-SNAPSHOT + 6.8.2-SNAPSHOT jar firebase-admin From 4d5ab956f3bceedbc1228d33e988fb10b324ac2b Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 20 May 2019 11:18:04 -0700 Subject: [PATCH 064/456] Deferring the initialization of FirebaseUserManager (#269) --- .../google/firebase/auth/FirebaseAuth.java | 25 ++++++++++++++++--- .../firebase/auth/FirebaseAuthTest.java | 14 ++--------- .../auth/FirebaseUserManagerTest.java | 8 +++++- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 0e02f8371..4c488cfea 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -67,16 +67,21 @@ public class FirebaseAuth { private final Supplier tokenFactory; private final Supplier idTokenVerifier; private final Supplier cookieVerifier; + private final Supplier userManager; private final JsonFactory jsonFactory; - private final FirebaseUserManager userManager; private FirebaseAuth(Builder builder) { this.firebaseApp = checkNotNull(builder.firebaseApp); this.tokenFactory = threadSafeMemoize(builder.tokenFactory); this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier); this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier); + this.userManager = threadSafeMemoize(new Supplier() { + @Override + public FirebaseUserManager get() { + return new FirebaseUserManager(firebaseApp); + } + }); this.jsonFactory = firebaseApp.getOptions().getJsonFactory(); - this.userManager = new FirebaseUserManager(firebaseApp); } /** @@ -139,6 +144,7 @@ private CallableOperation createSessionCookieOp( checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty"); checkNotNull(options, "options must not be null"); + final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override protected String execute() throws FirebaseAuthException { @@ -225,6 +231,7 @@ public FirebaseToken execute() throws FirebaseAuthException { FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) { FirebaseTokenVerifier verifier = cookieVerifier.get(); if (checkRevoked) { + FirebaseUserManager userManager = getUserManager(); verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager); } return verifier; @@ -432,6 +439,7 @@ protected FirebaseToken execute() throws FirebaseAuthException { FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) { FirebaseTokenVerifier verifier = idTokenVerifier.get(); if (checkRevoked) { + FirebaseUserManager userManager = getUserManager(); verifier = RevocationCheckDecorator.decorateIdTokenVerifier(verifier, userManager); } return verifier; @@ -472,6 +480,7 @@ public ApiFuture revokeRefreshTokensAsync(@NonNull String uid) { private CallableOperation revokeRefreshTokensOp(final String uid) { checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override protected Void execute() throws FirebaseAuthException { @@ -511,6 +520,7 @@ public ApiFuture getUserAsync(@NonNull String uid) { private CallableOperation getUserOp(final String uid) { checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override protected UserRecord execute() throws FirebaseAuthException { @@ -548,6 +558,7 @@ private CallableOperation getUserByEmailOp( final String email) { checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override protected UserRecord execute() throws FirebaseAuthException { @@ -585,6 +596,7 @@ private CallableOperation getUserByPhoneNumbe final String phoneNumber) { checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override protected UserRecord execute() throws FirebaseAuthException { @@ -652,6 +664,7 @@ public ApiFuture listUsersAsync(@Nullable String pageToken, int m private CallableOperation listUsersOp( @Nullable final String pageToken, final int maxResults) { checkNotDestroyed(); + final FirebaseUserManager userManager = getUserManager(); final PageFactory factory = new PageFactory( new DefaultUserSource(userManager, jsonFactory), maxResults, pageToken); return new CallableOperation() { @@ -692,6 +705,7 @@ private CallableOperation createUserOp( final CreateRequest request) { checkNotDestroyed(); checkNotNull(request, "create request must not be null"); + final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override protected UserRecord execute() throws FirebaseAuthException { @@ -731,6 +745,7 @@ private CallableOperation updateUserOp( final UpdateRequest request) { checkNotDestroyed(); checkNotNull(request, "update request must not be null"); + final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override protected UserRecord execute() throws FirebaseAuthException { @@ -783,6 +798,7 @@ private CallableOperation setCustomUserClaimsOp( final String uid, final Map claims) { checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override protected Void execute() throws FirebaseAuthException { @@ -820,6 +836,7 @@ public ApiFuture deleteUserAsync(String uid) { private CallableOperation deleteUserOp(final String uid) { checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override protected Void execute() throws FirebaseAuthException { @@ -901,6 +918,7 @@ private CallableOperation importUsersOp final List users, final UserImportOptions options) { checkNotDestroyed(); final UserImportRequest request = new UserImportRequest(users, options, jsonFactory); + final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override protected UserImportResult execute() throws FirebaseAuthException { @@ -1071,7 +1089,7 @@ public ApiFuture generateSignInWithEmailLinkAsync( @VisibleForTesting FirebaseUserManager getUserManager() { - return this.userManager; + return this.userManager.get(); } private CallableOperation generateEmailActionLinkOp( @@ -1081,6 +1099,7 @@ private CallableOperation generateEmailActionLink if (type == EmailLinkType.EMAIL_SIGNIN) { checkNotNull(settings, "ActionCodeSettings must not be null when generating sign-in links"); } + final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override protected String execute() throws FirebaseAuthException { diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index c495c41df..cf3b68385 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -46,7 +46,6 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; -import org.junit.Assert; import org.junit.Test; public class FirebaseAuthTest { @@ -139,21 +138,12 @@ public void testInitAfterAppDelete() throws ExecutionException, InterruptedExcep } @Test - public void testProjectIdRequired() { + public void testProjectIdNotRequiredAtInitialization() { FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .build(); FirebaseApp app = FirebaseApp.initializeApp(options, "testProjectIdRequired"); - try { - FirebaseAuth.getInstance(app); - fail("Expected exception."); - } catch (IllegalArgumentException expected) { - Assert.assertEquals( - "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()); - } + assertNotNull(FirebaseAuth.getInstance(app)); } @Test(expected = IllegalArgumentException.class) diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 72e81fd19..97ff7447a 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -85,10 +85,16 @@ public void testProjectIdRequired() { FirebaseApp.initializeApp(new FirebaseOptions.Builder() .setCredentials(credentials) .build()); + FirebaseAuth auth = FirebaseAuth.getInstance(); try { - FirebaseAuth.getInstance(); + auth.getUserManager(); fail("No error thrown for missing project ID"); } catch (IllegalArgumentException expected) { + assertEquals( + "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()); } } From dda8ff2d87b4c080cbeff7f907c64b98a89bed78 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 20 May 2019 11:18:57 -0700 Subject: [PATCH 065/456] Upgraded Firestore and other gcloud dependencies (#268) * Upgraded Firestore and other gcloud dependencies * Upgraded guava and google-api-client --- pom.xml | 24 +++++++------------ .../auth/FirebaseTokenVerifierImpl.java | 4 +++- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index 284e503ac..d2b28f01c 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.22.Final + 4.1.34.Final @@ -392,50 +392,44 @@ com.google.api-client google-api-client - 1.25.0 - - - com.google.guava - guava-jdk5 - - + 1.28.0 com.google.api-client google-api-client-gson - 1.25.0 + 1.28.0 com.google.http-client google-http-client - 1.25.0 + 1.28.0 com.google.api api-common - 1.7.0 + 1.8.1 com.google.auth google-auth-library-oauth2-http - 0.11.0 + 0.15.0 com.google.cloud google-cloud-storage - 1.43.0 + 1.71.0 com.google.cloud google-cloud-firestore - 0.61.0-beta + 1.1.0 com.google.guava guava - 20.0 + 26.0-android org.slf4j diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java index 1da2d7a34..c164173a6 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java @@ -128,7 +128,9 @@ private String prefixWithIndefiniteArticle(String word) { private IdToken parse(String token) throws FirebaseAuthException { try { return IdToken.parse(jsonFactory, token); - } catch (IOException e) { + } catch (IllegalArgumentException | IOException e) { + // Old versions of guava throw an IOException for invalid strings, while new versions + // might throw an IllegalArgumentException String detailedError = String.format( "Failed to parse Firebase %s. Make sure you passed a string that represents a complete " + "and valid JWT. See %s for details on how to retrieve %s.", From 64975779ff294e5c2f868a942b1ed8e2c139f9d8 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 26 Jun 2019 11:32:40 -0700 Subject: [PATCH 066/456] Upgrades Firestore and Storage dependencies (#282) --- CHANGELOG.md | 4 +++- pom.xml | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40c9c46aa..71672db4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Unreleased -- +- [changed] Upgraded the Cloud Firestore client to 1.9.0. +- [changed] Upgraded the Cloud Storage client to 1.79.0. +- [changed] Upgraded the Google API client to 1.30.1. # v6.8.1 diff --git a/pom.xml b/pom.xml index d2b28f01c..422dd0303 100644 --- a/pom.xml +++ b/pom.xml @@ -392,17 +392,17 @@ com.google.api-client google-api-client - 1.28.0 + 1.30.1 com.google.api-client google-api-client-gson - 1.28.0 + 1.30.1 com.google.http-client google-http-client - 1.28.0 + 1.30.1 com.google.api @@ -412,17 +412,17 @@ com.google.auth google-auth-library-oauth2-http - 0.15.0 + 0.16.1 com.google.cloud google-cloud-storage - 1.71.0 + 1.79.0 com.google.cloud google-cloud-firestore - 1.1.0 + 1.9.0 From 6e507af6fd84efcd7f8e79fc1561d92a0a72bbec Mon Sep 17 00:00:00 2001 From: Nuno Goncalves <1140358@isep.ipp.pt> Date: Wed, 26 Jun 2019 22:18:58 +0100 Subject: [PATCH 067/456] =?UTF-8?q?Change=20addAllTokens=20argument=20data?= =?UTF-8?q?=20type=20from=20List=20to=20Collection=20(fireb=E2=80=A6=20(#2?= =?UTF-8?q?81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change addAllTokens argument data type from List to Collection (firebase#280) * Fix Javadoc comment to reflect method argument type --- .../com/google/firebase/messaging/MulticastMessage.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/MulticastMessage.java b/src/main/java/com/google/firebase/messaging/MulticastMessage.java index 32c9365f1..16434f6a9 100644 --- a/src/main/java/com/google/firebase/messaging/MulticastMessage.java +++ b/src/main/java/com/google/firebase/messaging/MulticastMessage.java @@ -22,6 +22,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.internal.NonNull; + +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -113,13 +115,13 @@ public Builder addToken(@NonNull String token) { } /** - * Adds a list of tokens to which the message should be sent. Up to 100 tokens can be + * Adds a collection of tokens to which the message should be sent. Up to 100 tokens can be * specified on a single instance of {@link MulticastMessage}. * - * @param tokens List of Firebase device registration tokens. + * @param tokens Collection of Firebase device registration tokens. * @return This builder. */ - public Builder addAllTokens(@NonNull List tokens) { + public Builder addAllTokens(@NonNull Collection tokens) { this.tokens.addAll(tokens); return this; } From 62ac3fc7f8b8119923710c6bf57f1922c005f49e Mon Sep 17 00:00:00 2001 From: the-real-mrcs <52238803+the-real-mrcs@users.noreply.github.com> Date: Tue, 9 Jul 2019 17:06:03 +0100 Subject: [PATCH 068/456] Add support for analytics_label to be specified (#285) --- CHANGELOG.md | 2 + .../firebase/messaging/AndroidConfig.java | 14 +++ .../firebase/messaging/AndroidFcmOptions.java | 77 +++++++++++++++ .../google/firebase/messaging/ApnsConfig.java | 14 +++ .../firebase/messaging/ApnsFcmOptions.java | 77 +++++++++++++++ .../google/firebase/messaging/FcmOptions.java | 78 +++++++++++++++ .../firebase/messaging/FcmOptionsUtil.java | 45 +++++++++ .../google/firebase/messaging/Message.java | 19 ++++ .../firebase/messaging/MessageTest.java | 95 +++++++++++++++++++ 9 files changed, 421 insertions(+) create mode 100644 src/main/java/com/google/firebase/messaging/AndroidFcmOptions.java create mode 100644 src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java create mode 100644 src/main/java/com/google/firebase/messaging/FcmOptions.java create mode 100644 src/main/java/com/google/firebase/messaging/FcmOptionsUtil.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 71672db4c..ab91dd8df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- [added] Added `FcmOptions`, `ApnsFcmOptions` and `AndroidFcmOptions` to the + `FirebaseMessaging` API, which all provides a `setAnalyticsLabel()` method. - [changed] Upgraded the Cloud Firestore client to 1.9.0. - [changed] Upgraded the Cloud Storage client to 1.79.0. - [changed] Upgraded the Google API client to 1.30.1. diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index 065376a79..dc0a45526 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -49,6 +49,9 @@ public class AndroidConfig { @Key("notification") private final AndroidNotification notification; + @Key("fcm_options") + private final AndroidFcmOptions fcmOptions; + private AndroidConfig(Builder builder) { this.collapseKey = builder.collapseKey; if (builder.priority != null) { @@ -71,6 +74,7 @@ private AndroidConfig(Builder builder) { this.restrictedPackageName = builder.restrictedPackageName; this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data); this.notification = builder.notification; + this.fcmOptions = builder.fcmOptions; } /** @@ -98,6 +102,7 @@ public static class Builder { private String restrictedPackageName; private final Map data = new HashMap<>(); private AndroidNotification notification; + private AndroidFcmOptions fcmOptions; private Builder() {} @@ -187,6 +192,15 @@ public Builder setNotification(AndroidNotification notification) { return this; } + /** + * Sets the {@link AndroidFcmOptions}, which will override values set in the {@link FcmOptions} + * for Android messages. + */ + public Builder setFcmOptions(AndroidFcmOptions androidFcmOptions) { + this.fcmOptions = androidFcmOptions; + return this; + } + /** * Creates a new {@link AndroidConfig} instance from the parameters set on this builder. * diff --git a/src/main/java/com/google/firebase/messaging/AndroidFcmOptions.java b/src/main/java/com/google/firebase/messaging/AndroidFcmOptions.java new file mode 100644 index 000000000..cbe5dffdf --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/AndroidFcmOptions.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.api.client.util.Key; + +/** + * Represents the Android-specific FCM options that can be included in an {@link AndroidConfig}. + * Instances of this class are thread-safe and immutable. + */ +public final class AndroidFcmOptions { + + @Key("analytics_label") + private final String analyticsLabel; + + private AndroidFcmOptions(Builder builder) { + FcmOptionsUtil.checkAnalyticsLabel(builder.analyticsLabel); + this.analyticsLabel = builder.analyticsLabel; + } + + /** + * Creates a new {@link AndroidFcmOptions} with the specified analytics label. + * + * @param analyticsLabel An analytics label + * @return An AndroidFcmOptions with the analytics label set to the supplied value. + */ + public static AndroidFcmOptions withAnalyticsLabel(String analyticsLabel) { + return builder().setAnalyticsLabel(analyticsLabel).build(); + } + + /** + * Creates a new {@link AndroidFcmOptions.Builder}. + * + * @return A {@link AndroidFcmOptions.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String analyticsLabel; + + private Builder() {} + + /** + * @param analyticsLabel A string representing the analytics label used for Android messages. + * @return This builder + */ + public Builder setAnalyticsLabel(String analyticsLabel) { + this.analyticsLabel = analyticsLabel; + return this; + } + + /** + * Creates a new {@link AndroidFcmOptions} instance from the parameters set on this builder. + * + * @return A new {@link AndroidFcmOptions} instance. + * @throws IllegalArgumentException If any of the parameters set on the builder are invalid. + */ + public AndroidFcmOptions build() { + return new AndroidFcmOptions(this); + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/ApnsConfig.java b/src/main/java/com/google/firebase/messaging/ApnsConfig.java index 5e503c0ea..0e5de8619 100644 --- a/src/main/java/com/google/firebase/messaging/ApnsConfig.java +++ b/src/main/java/com/google/firebase/messaging/ApnsConfig.java @@ -38,6 +38,9 @@ public class ApnsConfig { @Key("payload") private final Map payload; + @Key("fcm_options") + private final ApnsFcmOptions fcmOptions; + private ApnsConfig(Builder builder) { checkArgument(builder.aps != null, "aps must be specified"); checkArgument(!builder.customData.containsKey("aps"), @@ -47,6 +50,7 @@ private ApnsConfig(Builder builder) { .putAll(builder.customData) .put("aps", builder.aps.getFields()) .build(); + this.fcmOptions = builder.fcmOptions; } /** @@ -63,6 +67,7 @@ public static class Builder { private final Map headers = new HashMap<>(); private final Map customData = new HashMap<>(); private Aps aps; + private ApnsFcmOptions fcmOptions; private Builder() {} @@ -123,6 +128,15 @@ public Builder putAllCustomData(@NonNull Map map) { return this; } + /** + * Sets the {@link ApnsFcmOptions}, which will override values set in the {@link FcmOptions} for + * APNS messages. + */ + public Builder setFcmOptions(ApnsFcmOptions apnsFcmOptions) { + this.fcmOptions = apnsFcmOptions; + return this; + } + /** * Creates a new {@link ApnsConfig} instance from the parameters set on this builder. * diff --git a/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java b/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java new file mode 100644 index 000000000..a49095cba --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.api.client.util.Key; + +/** + * Represents the APNS-specific FCM options that can be included in an {@link ApnsConfig}. Instances + * of this class are thread-safe and immutable. + */ +public final class ApnsFcmOptions { + + @Key("analytics_label") + private final String analyticsLabel; + + private ApnsFcmOptions(Builder builder) { + FcmOptionsUtil.checkAnalyticsLabel(builder.analyticsLabel); + this.analyticsLabel = builder.analyticsLabel; + } + + /** + * Creates a new {@link ApnsFcmOptions} with the specified analytics label. + * + * @param analyticsLabel An analytics label + * @return An ApnsFcmOptions with the analytics label set to the supplied value. + */ + public static ApnsFcmOptions withAnalyticsLabel(String analyticsLabel) { + return builder().setAnalyticsLabel(analyticsLabel).build(); + } + + /** + * Creates a new {@link ApnsFcmOptions.Builder}. + * + * @return An {@link ApnsFcmOptions.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String analyticsLabel; + + private Builder() {} + + /** + * @param analyticsLabel A string representing the analytics label used for APNS messages. + * @return This builder + */ + public Builder setAnalyticsLabel(String analyticsLabel) { + this.analyticsLabel = analyticsLabel; + return this; + } + + /** + * Creates a new {@link ApnsFcmOptions} instance from the parameters set on this builder. + * + * @return A new {@link ApnsFcmOptions} instance. + * @throws IllegalArgumentException If any of the parameters set on the builder are invalid. + */ + public ApnsFcmOptions build() { + return new ApnsFcmOptions(this); + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/FcmOptions.java b/src/main/java/com/google/firebase/messaging/FcmOptions.java new file mode 100644 index 000000000..996deb8d7 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/FcmOptions.java @@ -0,0 +1,78 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.api.client.util.Key; + +/** + * Represents the platform-independent FCM options that can be included in a {@link Message}. + * Instances of this class are thread-safe and immutable. + */ +public final class FcmOptions { + + @Key("analytics_label") + private final String analyticsLabel; + + private FcmOptions(Builder builder) { + FcmOptionsUtil.checkAnalyticsLabel(builder.analyticsLabel); + this.analyticsLabel = builder.analyticsLabel; + } + + /** + * Creates a new {@link FcmOptions} with the specified analytics label. + * + * @param analyticsLabel An analytics label + * @return An FcmOptions with the analytics label set to the supplied value. + */ + public static FcmOptions withAnalyticsLabel(String analyticsLabel) { + return new Builder().setAnalyticsLabel(analyticsLabel).build(); + } + + /** + * Creates a new {@link FcmOptions.Builder}. + * + * @return An {@link FcmOptions.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String analyticsLabel; + + private Builder() {} + + /** + * @param analyticsLabel A string representing the analytics label used for messages where no + * platform-specific analytics label has been specified. + * @return This builder + */ + public Builder setAnalyticsLabel(String analyticsLabel) { + this.analyticsLabel = analyticsLabel; + return this; + } + + /** + * Creates a new {@link FcmOptions} instance from the parameters set on this builder. + * + * @return A new {@link FcmOptions} instance. + * @throws IllegalArgumentException If any of the parameters set on the builder are invalid. + */ + public FcmOptions build() { + return new FcmOptions(this); + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/FcmOptionsUtil.java b/src/main/java/com/google/firebase/messaging/FcmOptionsUtil.java new file mode 100644 index 000000000..b16b2b8ed --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/FcmOptionsUtil.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 java.util.regex.Pattern; + +final class FcmOptionsUtil { + + /** + * Pattern matching a valid analytics labels. + */ + private static final Pattern ANALYTICS_LABEL_REGEX = Pattern.compile("^[a-zA-Z0-9-_.~%]{0,50}$"); + + /** + * Returns false if the supplied {@code analyticsLabel} has a disallowed format. + */ + private static boolean isValid(String analyticsLabel) { + return ANALYTICS_LABEL_REGEX.matcher(analyticsLabel).matches(); + } + + /** + * Validates the format of the supplied label. + * + * @throws IllegalArgumentException If the label is non-null and has a disallowed format. + */ + static void checkAnalyticsLabel(String analyticsLabel) { + checkArgument( + analyticsLabel == null || isValid(analyticsLabel), + "Analytics label must have format matching'^[a-zA-Z0-9-_.~%]{1,50}$"); + } +} diff --git a/src/main/java/com/google/firebase/messaging/Message.java b/src/main/java/com/google/firebase/messaging/Message.java index 89a4a5ce3..c69abaeef 100644 --- a/src/main/java/com/google/firebase/messaging/Message.java +++ b/src/main/java/com/google/firebase/messaging/Message.java @@ -63,6 +63,9 @@ public class Message { @Key("condition") private final String condition; + @Key("fcm_options") + private final FcmOptions fcmOptions; + private Message(Builder builder) { this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data); this.notification = builder.notification; @@ -78,6 +81,7 @@ private Message(Builder builder) { this.token = builder.token; this.topic = stripPrefix(builder.topic); this.condition = builder.condition; + this.fcmOptions = builder.fcmOptions; } @VisibleForTesting @@ -120,6 +124,11 @@ String getCondition() { return condition; } + @VisibleForTesting + FcmOptions getFcmOptions() { + return fcmOptions; + } + Map wrapForTransport(boolean dryRun) { ImmutableMap.Builder payload = ImmutableMap.builder() .put("message", this); @@ -160,6 +169,7 @@ public static class Builder { private String token; private String topic; private String condition; + private FcmOptions fcmOptions; private Builder() {} @@ -266,6 +276,15 @@ public Builder putAllData(@NonNull Map map) { return this; } + /** + * Sets the {@link FcmOptions}, which can be overridden by the platform-specific {@code + * fcm_options} fields. + */ + public Builder setFcmOptions(FcmOptions fcmOptions) { + this.fcmOptions = fcmOptions; + return this; + } + /** * Creates a new {@link Message} instance from the parameters set on this builder. * diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 788127da8..5f555fe3d 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -583,6 +583,101 @@ public void testWrapForTransport() { assertSame(message, wrappedMessage.get("message")); } + @Test + public void testMessageWithAllFcmOptions() throws IOException { + Message messageUsingShorthand = Message.builder() + .setTopic("foo") + .setFcmOptions(FcmOptions.withAnalyticsLabel("message-label")) + .setAndroidConfig(AndroidConfig.builder() + .setFcmOptions(AndroidFcmOptions.withAnalyticsLabel("android-label")).build()) + .setApnsConfig( + ApnsConfig.builder().setAps(Aps.builder().build()) + .setFcmOptions(ApnsFcmOptions.withAnalyticsLabel("apns-label")) + .build()).build(); + Message messageUsingBuilder = Message.builder() + .setTopic("foo") + .setFcmOptions(FcmOptions.builder().setAnalyticsLabel("message-label").build()) + .setAndroidConfig(AndroidConfig.builder() + .setFcmOptions(AndroidFcmOptions.builder().setAnalyticsLabel("android-label").build()) + .build()) + .setApnsConfig( + ApnsConfig.builder().setAps(Aps.builder().build()) + .setFcmOptions(ApnsFcmOptions.builder().setAnalyticsLabel("apns-label").build()) + .build()).build(); + + ImmutableMap> androidConfig = + ImmutableMap.of("fcm_options", ImmutableMap.of("analytics_label", "android-label")); + ImmutableMap apnsConfig = + ImmutableMap.builder() + .put("fcm_options", ImmutableMap.of("analytics_label", "apns-label")) + .put("payload", ImmutableMap.of("aps", ImmutableMap.of())) + .build(); + ImmutableMap expected = + ImmutableMap.builder() + .put("topic", "foo") + .put("fcm_options", ImmutableMap.of("analytics_label", "message-label")) + .put("android", androidConfig) + .put("apns", apnsConfig) + .build(); + assertJsonEquals(expected, messageUsingBuilder); + assertJsonEquals(expected, messageUsingShorthand); + } + + @Test + public void createMessageWithDefaultFcmOptions() throws IOException { + Message message = Message.builder() + .setTopic("foo") + .setFcmOptions(FcmOptions.builder().build()) + .setAndroidConfig( + AndroidConfig.builder().setFcmOptions(AndroidFcmOptions.builder().build()).build()) + .setApnsConfig( + ApnsConfig.builder() + .setAps(Aps.builder().build()) + .setFcmOptions(ApnsFcmOptions.builder().build()) + .build()) + .build(); + + ImmutableMap apnsConfig = + ImmutableMap.builder() + .put("fcm_options", ImmutableMap.of()) + .put("payload", ImmutableMap.of("aps", ImmutableMap.of())) + .build(); + ImmutableMap expected = + ImmutableMap.builder() + .put("topic", "foo") + .put("fcm_options", ImmutableMap.of()) + .put("android", ImmutableMap.of("fcm_options", ImmutableMap.of())) + .put("apns", apnsConfig) + .build(); + assertJsonEquals(expected, message); + } + + @Test + public void testIncorrectAnalyticsLabelFormat() { + try { + FcmOptions.builder().setAnalyticsLabel("!").build(); + fail("No error thrown when using bad analytics label format."); + } catch (IllegalArgumentException expected) { + //expected + } + + try { + FcmOptions.builder() + .setAnalyticsLabel("THIS_IS_LONGER_THAN_50_CHARACTERS_WHICH_IS_NOT_ALLOWED") + .build(); + fail("No error thrown when using bad analytics label format."); + } catch (IllegalArgumentException expected) { + //expected + } + + try { + FcmOptions.builder().setAnalyticsLabel(" ").build(); + fail("No error thrown when using bad analytics label format."); + } catch (IllegalArgumentException expected) { + //expected + } + } + private static void assertJsonEquals( Map expected, Object actual) throws IOException { assertEquals(expected, toMap(actual)); From e83717fe0e7ad5ee38a618b4db756365fbf3bca6 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 17 Jul 2019 15:01:33 -0700 Subject: [PATCH 069/456] Updating book file path in generated API docs (#289) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 422dd0303..511cbe09b 100644 --- a/pom.xml +++ b/pom.xml @@ -123,7 +123,7 @@ - -hdf book.path /_book.yaml + -hdf book.path /docs/reference/_book.yaml -hdf project.path /_project.yaml -hdf devsite.path /docs/reference/admin/java/reference/ -d ${project.build.directory}/apidocs From 5a7132fabbb663af02171b79ac15f867e56eb77f Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 18 Jul 2019 14:35:21 -0700 Subject: [PATCH 070/456] Staged Release 6.9.0 (#290) * Updating CHANGELOG for 6.9.0 release. * [maven-release-plugin] prepare release v6.9.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 ab91dd8df..ca37dfd4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +- + +# v6.9.0 + - [added] Added `FcmOptions`, `ApnsFcmOptions` and `AndroidFcmOptions` to the `FirebaseMessaging` API, which all provides a `setAnalyticsLabel()` method. - [changed] Upgraded the Cloud Firestore client to 1.9.0. diff --git a/pom.xml b/pom.xml index 511cbe09b..c16717c11 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.8.2-SNAPSHOT + 6.9.1-SNAPSHOT jar firebase-admin From e6cf26cdb8970a26c9deeece4147263cd0f41d2e Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 25 Jul 2019 12:08:16 -0700 Subject: [PATCH 071/456] Updated FcmOptions Javadoc (#291) --- .../java/com/google/firebase/messaging/AndroidFcmOptions.java | 4 ++-- .../java/com/google/firebase/messaging/ApnsFcmOptions.java | 4 ++-- src/main/java/com/google/firebase/messaging/FcmOptions.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidFcmOptions.java b/src/main/java/com/google/firebase/messaging/AndroidFcmOptions.java index cbe5dffdf..82c29603f 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidFcmOptions.java +++ b/src/main/java/com/google/firebase/messaging/AndroidFcmOptions.java @@ -31,10 +31,10 @@ private AndroidFcmOptions(Builder builder) { } /** - * Creates a new {@link AndroidFcmOptions} with the specified analytics label. + * Creates a new {@link AndroidFcmOptions} object with the specified analytics label. * * @param analyticsLabel An analytics label - * @return An AndroidFcmOptions with the analytics label set to the supplied value. + * @return An {@link AndroidFcmOptions} object with the analytics label set to the supplied value. */ public static AndroidFcmOptions withAnalyticsLabel(String analyticsLabel) { return builder().setAnalyticsLabel(analyticsLabel).build(); diff --git a/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java b/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java index a49095cba..708c0b56d 100644 --- a/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java +++ b/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java @@ -31,10 +31,10 @@ private ApnsFcmOptions(Builder builder) { } /** - * Creates a new {@link ApnsFcmOptions} with the specified analytics label. + * Creates a new {@link ApnsFcmOptions} object with the specified analytics label. * * @param analyticsLabel An analytics label - * @return An ApnsFcmOptions with the analytics label set to the supplied value. + * @return An {@link ApnsFcmOptions} object with the analytics label set to the supplied value. */ public static ApnsFcmOptions withAnalyticsLabel(String analyticsLabel) { return builder().setAnalyticsLabel(analyticsLabel).build(); diff --git a/src/main/java/com/google/firebase/messaging/FcmOptions.java b/src/main/java/com/google/firebase/messaging/FcmOptions.java index 996deb8d7..1d32b9c51 100644 --- a/src/main/java/com/google/firebase/messaging/FcmOptions.java +++ b/src/main/java/com/google/firebase/messaging/FcmOptions.java @@ -31,10 +31,10 @@ private FcmOptions(Builder builder) { } /** - * Creates a new {@link FcmOptions} with the specified analytics label. + * Creates a new {@link FcmOptions} object with the specified analytics label. * * @param analyticsLabel An analytics label - * @return An FcmOptions with the analytics label set to the supplied value. + * @return An {@link FcmOptions} object with the analytics label set to the supplied value. */ public static FcmOptions withAnalyticsLabel(String analyticsLabel) { return new Builder().setAnalyticsLabel(analyticsLabel).build(); From 0e27bb0f949a5caf932b1d089ed7dda845fa6e86 Mon Sep 17 00:00:00 2001 From: chong-shao <31256040+chong-shao@users.noreply.github.com> Date: Fri, 2 Aug 2019 15:20:14 -0700 Subject: [PATCH 072/456] Fix a typo in the comment (#297) --- .../java/com/google/firebase/messaging/AndroidNotification.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index 0a6cd0620..064f493c2 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -138,7 +138,7 @@ public Builder setTitle(String title) { } /** - * Sets the body of the Android notification. When provided, overrides the body sent + * Sets the body of the Android notification. When provided, overrides the body set * via {@link Notification}. * * @param body Body of the notification. From 0a061592cf66f3379173c86d74680867d5974ee3 Mon Sep 17 00:00:00 2001 From: kirmerzlikin Date: Wed, 7 Aug 2019 00:04:09 +0300 Subject: [PATCH 073/456] Add missing WebpushFcmOptions entity (as per documentation) (#295) * Add missing WebpushFcmOptions entity As per https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#webpushfcmoptions * Improve public API of WebpushFcmOptions * Clean up code. Update CHANGELOG * Slightly rephrase change description in CHANGELOG --- CHANGELOG.md | 3 +- .../firebase/messaging/WebpushConfig.java | 18 ++++++ .../firebase/messaging/WebpushFcmOptions.java | 62 +++++++++++++++++++ .../FirebaseMessagingClientImplTest.java | 4 +- .../messaging/FirebaseMessagingIT.java | 1 + .../firebase/messaging/MessageTest.java | 20 ++++++ .../snippets/FirebaseMessagingSnippets.java | 4 +- 7 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/google/firebase/messaging/WebpushFcmOptions.java diff --git a/CHANGELOG.md b/CHANGELOG.md index ca37dfd4a..d95bd8e71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased -- +- [added] Added `WebpushFcmOptions` to the `FirebaseMessaging` API, providing + the `setLink()` method. # v6.9.0 diff --git a/src/main/java/com/google/firebase/messaging/WebpushConfig.java b/src/main/java/com/google/firebase/messaging/WebpushConfig.java index 7c99580c6..9cde9d041 100644 --- a/src/main/java/com/google/firebase/messaging/WebpushConfig.java +++ b/src/main/java/com/google/firebase/messaging/WebpushConfig.java @@ -19,6 +19,7 @@ import com.google.api.client.util.Key; import com.google.common.collect.ImmutableMap; import com.google.firebase.internal.NonNull; + import java.util.HashMap; import java.util.Map; @@ -37,10 +38,14 @@ public class WebpushConfig { @Key("notification") private final Map notification; + @Key("fcm_options") + private final WebpushFcmOptions fcmOptions; + 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 != null ? builder.notification.getFields() : null; + this.fcmOptions = builder.fcmOptions; } /** @@ -57,6 +62,8 @@ public static class Builder { private final Map headers = new HashMap<>(); private final Map data = new HashMap<>(); private WebpushNotification notification; + private WebpushFcmOptions fcmOptions; + private Builder() {} @@ -125,6 +132,17 @@ public Builder setNotification(WebpushNotification notification) { return this; } + /** + * Sets the Webpush FCM options to be included in the Webpush config. + * + * @param fcmOptions A {@link WebpushFcmOptions} instance. + * @return This builder. + */ + public Builder setFcmOptions(WebpushFcmOptions fcmOptions) { + this.fcmOptions = fcmOptions; + return this; + } + /** * Creates a new {@link WebpushConfig} instance from the parameters set on this builder. * diff --git a/src/main/java/com/google/firebase/messaging/WebpushFcmOptions.java b/src/main/java/com/google/firebase/messaging/WebpushFcmOptions.java new file mode 100644 index 000000000..bef49ec10 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/WebpushFcmOptions.java @@ -0,0 +1,62 @@ +package com.google.firebase.messaging; + +import com.google.api.client.util.Key; + +/** + * Represents options for features provided by the FCM SDK for Web. + * Can be included in {@link WebpushConfig}. Instances of this class are thread-safe and immutable. + */ +public final class WebpushFcmOptions { + + @Key("link") + private final String link; + + private WebpushFcmOptions(Builder builder) { + this.link = builder.link; + } + + /** + * Creates a new {@code WebpushFcmOptions} using given link. + * + * @param link The link to open when the user clicks on the notification. + * For all URL values, HTTPS is required. + */ + public static WebpushFcmOptions withLink(String link) { + return new Builder().setLink(link).build(); + } + + /** + * Creates a new {@link WebpushFcmOptions.Builder}. + * + * @return An {@link WebpushFcmOptions.Builder} instance. + */ + public static Builder builder() { + return new WebpushFcmOptions.Builder(); + } + + public static class Builder { + + private String link; + + private Builder() {} + + /** + * @param link The link to open when the user clicks on the notification. + * For all URL values, HTTPS is required. + * @return This builder + */ + public Builder setLink(String link) { + this.link = link; + return this; + } + + /** + * Creates a new {@link WebpushFcmOptions} instance from the parameters set on this builder. + * + * @return A new {@link WebpushFcmOptions} instance. + */ + public WebpushFcmOptions build() { + return new WebpushFcmOptions(this); + } + } +} diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java index 87e9499cc..ae1136bf6 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -781,6 +781,7 @@ private static Map> buildTestMessages() { .putCustomData("k4", "v4") .putAllCustomData(ImmutableMap.of("k5", "v5", "k6", "v6")) .build()) + .setFcmOptions(WebpushFcmOptions.withLink("https://firebase.google.com")) .build()) .setTopic("test-topic") .build(), @@ -811,7 +812,8 @@ private static Map> buildTestMessages() { .put("k4", "v4") .put("k5", "v5") .put("k6", "v6") - .build()) + .build(), + "fcm_options", ImmutableMap.of("link", "https://firebase.google.com")) )); return builder.build(); diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 9a2b3ff83..751ba8bdd 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -59,6 +59,7 @@ public void testSend() throws Exception { .setWebpushConfig(WebpushConfig.builder() .putHeader("X-Custom-Val", "Foo") .setNotification(new WebpushNotification("Title", "Body")) + .setFcmOptions(WebpushFcmOptions.withLink("https://firebase.google.com")) .build()) .setTopic("foo-bar") .build(); diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 5f555fe3d..6744a35f6 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -271,6 +271,26 @@ public void testWebpushMessageWithoutNotification() throws IOException { assertJsonEquals(ImmutableMap.of("topic", "test-topic", "webpush", data), message); } + @Test + public void testWebpushMessageWithWebpushOptions() throws IOException { + Message message = Message.builder() + .setWebpushConfig(WebpushConfig.builder() + .putHeader("k1", "v1") + .putAllHeaders(ImmutableMap.of("k2", "v2", "k3", "v3")) + .putData("k1", "v1") + .putAllData(ImmutableMap.of("k2", "v2", "k3", "v3")) + .setFcmOptions(WebpushFcmOptions.withLink("https://my-server/page")) + .build()) + .setTopic("test-topic") + .build(); + Map data = ImmutableMap.of( + "headers", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), + "data", ImmutableMap.of("k1", "v1", "k2", "v2", "k3", "v3"), + "fcm_options", ImmutableMap.of("link", "https://my-server/page") + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "webpush", data), message); + } + @Test public void testWebpushMessageWithNotification() throws IOException { Message message = Message.builder() diff --git a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java index 7c46ad9ac..eb940215a 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java @@ -30,6 +30,7 @@ import com.google.firebase.messaging.SendResponse; import com.google.firebase.messaging.TopicManagementResponse; import com.google.firebase.messaging.WebpushConfig; +import com.google.firebase.messaging.WebpushFcmOptions; import com.google.firebase.messaging.WebpushNotification; import java.util.ArrayList; import java.util.Arrays; @@ -239,6 +240,7 @@ public Message webpushMessage() { "$GOOG up 1.43% on the day", "$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.", "https://my-server/icon.png")) + .setFcmOptions(WebpushFcmOptions.withLink("https://my-server/page-to-open-on-click")) .build()) .setTopic("industry-tech") .build(); @@ -310,4 +312,4 @@ public void unsubscribeFromTopic() throws FirebaseMessagingException { // [END unsubscribe] } -} \ No newline at end of file +} From 9ace7b1f547a0b28187e908fc5e6e5e600ad563c Mon Sep 17 00:00:00 2001 From: Saraj Munjal Date: Fri, 23 Aug 2019 14:42:21 -0700 Subject: [PATCH 074/456] feat(rtdb): Connect to RTDB emulator when valid emulator URL is passed OR env vars are set correctly (#299) * Talk to RTDB emulator instead of database instance if FIREBASE_RTDB_EMULATOR_HOST=true * Added test for talking to rtdb emulator when vars are set * Updated code and added tests to reflect valid emulator URLs and the logic for inferring them correctly * Remove stray character * Renamed setup and teardown methods to be correct * Addressed most review comments, moved emulator URL override check to FirebaseDatabase only, updated test cases * Added deferred credentials functionality * Verified and fixed parsing of encoded URLs * Fixed another round of reveiw comments * Fixed checkstyle errors * Allowed HTTPS connections to the emulator, only if the user passes in a valid URL through the databaseUrl field * Minor changes * Minor fixes * Fixes from review comments * Fixed test imports * Handled + in URLs and added test * Inlined variable declarations --- .../java/com/google/firebase/FirebaseApp.java | 9 +- .../com/google/firebase/FirebaseOptions.java | 46 ++- .../firebase/database/FirebaseDatabase.java | 56 +++- .../connection/NettyWebSocketClient.java | 25 +- .../connection/WebsocketConnection.java | 4 +- .../firebase/database/core/Context.java | 10 + .../database/core/JvmAuthTokenProvider.java | 7 +- .../database/util/EmulatorHelper.java | 70 +++++ .../database/utilities/Utilities.java | 99 +++--- .../com/google/firebase/FirebaseAppTest.java | 9 + .../google/firebase/FirebaseOptionsTest.java | 4 +- .../database/FirebaseDatabaseTest.java | 287 +++++++++++++----- .../database/util/EmulatorHelperTest.java | 134 ++++++++ .../database/utilities/UtilitiesTest.java | 29 ++ .../google/firebase/testing/TestUtils.java | 25 ++ 15 files changed, 664 insertions(+), 150 deletions(-) create mode 100644 src/main/java/com/google/firebase/database/util/EmulatorHelper.java create mode 100644 src/test/java/com/google/firebase/database/util/EmulatorHelperTest.java diff --git a/src/main/java/com/google/firebase/FirebaseApp.java b/src/main/java/com/google/firebase/FirebaseApp.java index c7f24476b..f69ee36c9 100644 --- a/src/main/java/com/google/firebase/FirebaseApp.java +++ b/src/main/java/com/google/firebase/FirebaseApp.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static com.google.firebase.FirebaseOptions.APPLICATION_DEFAULT_CREDENTIALS; import com.google.api.client.googleapis.util.Utils; import com.google.api.client.json.JsonFactory; @@ -33,6 +34,7 @@ import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; +import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.firebase.internal.FirebaseAppStore; import com.google.firebase.internal.FirebaseScheduledExecutor; @@ -582,10 +584,9 @@ private static FirebaseOptions getOptionsFromEnvironment() throws IOException { String defaultConfig = System.getenv(FIREBASE_CONFIG_ENV_VAR); if (Strings.isNullOrEmpty(defaultConfig)) { return new FirebaseOptions.Builder() - .setCredentials(GoogleCredentials.getApplicationDefault()) - .build(); + .setCredentials(APPLICATION_DEFAULT_CREDENTIALS) + .build(); } - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); FirebaseOptions.Builder builder = new FirebaseOptions.Builder(); JsonParser parser; @@ -597,7 +598,7 @@ private static FirebaseOptions getOptionsFromEnvironment() throws IOException { parser = jsonFactory.createJsonParser(reader); } parser.parseAndClose(builder); - builder.setCredentials(GoogleCredentials.getApplicationDefault()); + builder.setCredentials(APPLICATION_DEFAULT_CREDENTIALS); return builder.build(); } } diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java index 49c068e5d..b93d92f32 100644 --- a/src/main/java/com/google/firebase/FirebaseOptions.java +++ b/src/main/java/com/google/firebase/FirebaseOptions.java @@ -26,11 +26,14 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.firestore.FirestoreOptions; import com.google.common.base.Strings; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.firebase.internal.FirebaseThreadManagers; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; +import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -56,9 +59,21 @@ public final class FirebaseOptions { "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/datastore"); + static final Supplier APPLICATION_DEFAULT_CREDENTIALS = + new Supplier() { + @Override + public GoogleCredentials get() { + try { + return GoogleCredentials.getApplicationDefault().createScoped(FIREBASE_SCOPES); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + }; + private final String databaseUrl; private final String storageBucket; - private final GoogleCredentials credentials; + private final Supplier credentialsSupplier; private final Map databaseAuthVariableOverride; private final String projectId; private final String serviceAccountId; @@ -69,11 +84,10 @@ public final class FirebaseOptions { private final ThreadManager threadManager; private final FirestoreOptions firestoreOptions; - private FirebaseOptions(@NonNull FirebaseOptions.Builder builder) { - this.credentials = checkNotNull(builder.credentials, - "FirebaseOptions must be initialized with setCredentials().") - .createScoped(FIREBASE_SCOPES); + private FirebaseOptions(@NonNull final FirebaseOptions.Builder builder) { this.databaseUrl = builder.databaseUrl; + this.credentialsSupplier = checkNotNull( + builder.credentialsSupplier, "FirebaseOptions must be initialized with setCredentials()."); this.databaseAuthVariableOverride = builder.databaseAuthVariableOverride; this.projectId = builder.projectId; if (!Strings.isNullOrEmpty(builder.storageBucket)) { @@ -118,7 +132,7 @@ public String getStorageBucket() { } GoogleCredentials getCredentials() { - return credentials; + return credentialsSupplier.get(); } /** @@ -227,8 +241,7 @@ public static final class Builder { @Key("serviceAccountId") private String serviceAccountId; - - private GoogleCredentials credentials; + private Supplier credentialsSupplier; private FirestoreOptions firestoreOptions; private HttpTransport httpTransport = Utils.getDefaultTransport(); private JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); @@ -248,7 +261,7 @@ public Builder() {} public Builder(FirebaseOptions options) { databaseUrl = options.databaseUrl; storageBucket = options.storageBucket; - credentials = options.credentials; + credentialsSupplier = options.credentialsSupplier; databaseAuthVariableOverride = options.databaseAuthVariableOverride; projectId = options.projectId; httpTransport = options.httpTransport; @@ -308,7 +321,20 @@ public Builder setStorageBucket(String storageBucket) { * @return This Builder instance is returned so subsequent calls can be chained. */ public Builder setCredentials(GoogleCredentials credentials) { - this.credentials = checkNotNull(credentials); + this.credentialsSupplier = Suppliers + .ofInstance(checkNotNull(credentials).createScoped(FIREBASE_SCOPES)); + return this; + } + + /** + * Sets the Supplier of GoogleCredentials to use to authenticate the + * SDK. This is NOT intended for public use outside the SDK. + * + * @param credentialsSupplier Supplier instance that wraps GoogleCredentials. + * @return This Builder instance is returned so subsequent calls can be chained. + */ + Builder setCredentials(Supplier credentialsSupplier) { + this.credentialsSupplier = checkNotNull(credentialsSupplier); return this; } diff --git a/src/main/java/com/google/firebase/database/FirebaseDatabase.java b/src/main/java/com/google/firebase/database/FirebaseDatabase.java index 2ddb55d58..0a79932db 100644 --- a/src/main/java/com/google/firebase/database/FirebaseDatabase.java +++ b/src/main/java/com/google/firebase/database/FirebaseDatabase.java @@ -19,6 +19,10 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.ImplFirebaseTrampolines; @@ -27,15 +31,20 @@ import com.google.firebase.database.core.Repo; import com.google.firebase.database.core.RepoInfo; import com.google.firebase.database.core.RepoManager; +import com.google.firebase.database.util.EmulatorHelper; import com.google.firebase.database.utilities.ParsedUrl; import com.google.firebase.database.utilities.Utilities; import com.google.firebase.database.utilities.Validation; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.SdkUtils; +import java.io.IOException; import java.util.Collections; +import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -112,15 +121,21 @@ public static synchronized FirebaseDatabase getInstance(FirebaseApp app, String if (service == null) { service = ImplFirebaseTrampolines.addService(app, new FirebaseDatabaseService()); } - - DatabaseInstances dbInstances = service.getInstance(); if (url == null || url.isEmpty()) { throw new DatabaseException( "Failed to get FirebaseDatabase instance: Specify DatabaseURL within " + "FirebaseApp or from your getInstance() call."); } - - ParsedUrl parsedUrl = Utilities.parseUrl(url); + ParsedUrl parsedUrl; + boolean connectingToEmulator = false; + String possibleEmulatorUrl = EmulatorHelper + .getEmulatorUrl(url, EmulatorHelper.getEmulatorHostFromEnv()); + if (!Strings.isNullOrEmpty(possibleEmulatorUrl)) { + parsedUrl = Utilities.parseUrl(possibleEmulatorUrl); + connectingToEmulator = true; + } else { + parsedUrl = Utilities.parseUrl(url); + } if (!parsedUrl.path.isEmpty()) { throw new DatabaseException( "Specified Database URL '" @@ -130,6 +145,7 @@ public static synchronized FirebaseDatabase getInstance(FirebaseApp app, String + parsedUrl.path.toString()); } + DatabaseInstances dbInstances = service.getInstance(); FirebaseDatabase database = dbInstances.get(parsedUrl.repoInfo); if (database == null) { DatabaseConfig config = new DatabaseConfig(); @@ -140,11 +156,12 @@ public static synchronized FirebaseDatabase getInstance(FirebaseApp app, String config.setSessionPersistenceKey(app.getName()); } config.setFirebaseApp(app); - + if (connectingToEmulator) { + config.setCustomCredentials(new EmulatorCredentials(), true); + } database = new FirebaseDatabase(app, parsedUrl.repoInfo, config); dbInstances.put(parsedUrl.repoInfo, database); } - return database; } @@ -207,6 +224,11 @@ public DatabaseReference getReference(String path) { public DatabaseReference getReferenceFromUrl(String url) { checkNotNull(url, "Can't pass null for argument 'url' in FirebaseDatabase.getReferenceFromUrl()"); + String possibleEmulatorUrl = EmulatorHelper + .getEmulatorUrl(url, EmulatorHelper.getEmulatorHostFromEnv()); + if (!Strings.isNullOrEmpty(possibleEmulatorUrl)) { + url = possibleEmulatorUrl; + } ParsedUrl parsedUrl = Utilities.parseUrl(url); Repo repo = ensureRepo(); if (!parsedUrl.repoInfo.host.equals(repo.getRepoInfo().host)) { @@ -380,4 +402,26 @@ public void destroy() { instance.destroy(); } } + + private static class EmulatorCredentials extends GoogleCredentials { + + EmulatorCredentials() { + super(newToken()); + } + + private static AccessToken newToken() { + return new AccessToken("owner", + new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1))); + } + + @Override + public AccessToken refreshAccessToken() { + return newToken(); + } + + @Override + public Map> getRequestMetadata() throws IOException { + return ImmutableMap.of(); + } + } } diff --git a/src/main/java/com/google/firebase/database/connection/NettyWebSocketClient.java b/src/main/java/com/google/firebase/database/connection/NettyWebSocketClient.java index 9769b65a8..403a1319f 100644 --- a/src/main/java/com/google/firebase/database/connection/NettyWebSocketClient.java +++ b/src/main/java/com/google/firebase/database/connection/NettyWebSocketClient.java @@ -35,8 +35,11 @@ import java.io.EOFException; import java.net.URI; import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadFactory; +import javax.net.ssl.SSLException; import javax.net.ssl.TrustManagerFactory; /** @@ -57,13 +60,15 @@ class NettyWebSocketClient implements WebsocketConnection.WSClient { private final ChannelHandler channelHandler; private final ExecutorService executorService; private final EventLoopGroup group; + private final boolean isSecure; private Channel channel; NettyWebSocketClient( - URI uri, String userAgent, ThreadFactory threadFactory, + URI uri, boolean isSecure, String userAgent, ThreadFactory threadFactory, WebsocketConnection.WSClientEventHandler eventHandler) { this.uri = checkNotNull(uri, "uri must not be null"); + this.isSecure = isSecure; this.eventHandler = checkNotNull(eventHandler, "event handler must not be null"); this.channelHandler = new WebSocketClientHandler(uri, userAgent, eventHandler); this.executorService = new FirebaseScheduledExecutor(threadFactory, @@ -75,20 +80,26 @@ class NettyWebSocketClient implements WebsocketConnection.WSClient { public void connect() { checkState(channel == null, "channel already initialized"); try { - TrustManagerFactory trustFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); - trustFactory.init((KeyStore) null); - final SslContext sslContext = SslContextBuilder.forClient() - .trustManager(trustFactory).build(); Bootstrap bootstrap = new Bootstrap(); + SslContext sslContext = null; + if (this.isSecure) { + TrustManagerFactory trustFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustFactory.init((KeyStore) null); + sslContext = SslContextBuilder.forClient() + .trustManager(trustFactory).build(); + } final int port = uri.getPort() != -1 ? uri.getPort() : DEFAULT_WSS_PORT; + final SslContext finalSslContext = sslContext; bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) { ChannelPipeline p = ch.pipeline(); - p.addLast(sslContext.newHandler(ch.alloc(), uri.getHost(), port)); + if (finalSslContext != null) { + p.addLast(finalSslContext.newHandler(ch.alloc(), uri.getHost(), port)); + } p.addLast( new HttpClientCodec(), // Set the max size for the HTTP responses. This only applies to the WebSocket diff --git a/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java b/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java index 3e29db07a..a421b90d4 100644 --- a/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java +++ b/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java @@ -413,8 +413,8 @@ public WSClient newClient(WSClientEventHandler delegate) { String host = (optCachedHost != null) ? optCachedHost : hostInfo.getHost(); URI uri = HostInfo.getConnectionUrl( host, hostInfo.isSecure(), hostInfo.getNamespace(), optLastSessionId); - return new NettyWebSocketClient( - uri, context.getUserAgent(), context.getThreadFactory(), delegate); + return new NettyWebSocketClient(uri, hostInfo.isSecure(), context.getUserAgent(), + context.getThreadFactory(), delegate); } } diff --git a/src/main/java/com/google/firebase/database/core/Context.java b/src/main/java/com/google/firebase/database/core/Context.java index da68861cc..a0025f997 100644 --- a/src/main/java/com/google/firebase/database/core/Context.java +++ b/src/main/java/com/google/firebase/database/core/Context.java @@ -16,6 +16,7 @@ package com.google.firebase.database.core; +import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.database.DatabaseException; @@ -233,4 +234,13 @@ private String buildUserAgent(String platformAgent) { .append(platformAgent); return sb.toString(); } + + public void setCustomCredentials(GoogleCredentials customCredentials, boolean autoRefresh) { + // ensure that platform exists + getPlatform(); + // ensure that runloop exists else we might get a NPE + this.ensureRunLoop(); + this.authTokenProvider = new JvmAuthTokenProvider(firebaseApp, this.getExecutorService(), + autoRefresh, customCredentials); + } } diff --git a/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java b/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java index 541a4e4ee..a1cb79688 100644 --- a/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java +++ b/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java @@ -41,7 +41,12 @@ public class JvmAuthTokenProvider implements AuthTokenProvider { } JvmAuthTokenProvider(FirebaseApp firebaseApp, Executor executor, boolean autoRefresh) { - this.credentials = ImplFirebaseTrampolines.getCredentials(firebaseApp); + this(firebaseApp, executor, autoRefresh, ImplFirebaseTrampolines.getCredentials(firebaseApp)); + } + + JvmAuthTokenProvider(FirebaseApp firebaseApp, Executor executor, boolean autoRefresh, + GoogleCredentials customCredentials) { + this.credentials = customCredentials; this.authVariable = firebaseApp.getOptions().getDatabaseAuthVariableOverride(); this.executor = executor; if (autoRefresh) { diff --git a/src/main/java/com/google/firebase/database/util/EmulatorHelper.java b/src/main/java/com/google/firebase/database/util/EmulatorHelper.java new file mode 100644 index 000000000..833f9760e --- /dev/null +++ b/src/main/java/com/google/firebase/database/util/EmulatorHelper.java @@ -0,0 +1,70 @@ +/* + * Copyright 2019 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.util; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.firebase.database.annotations.Nullable; +import com.google.firebase.database.core.RepoInfo; +import com.google.firebase.database.utilities.ParsedUrl; +import com.google.firebase.database.utilities.Utilities; + +public final class EmulatorHelper { + + private EmulatorHelper() { + } + + @VisibleForTesting + public static final String FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR = "FIREBASE_RTDB_EMULATOR_HOST"; + + public static String getEmulatorHostFromEnv() { + return System.getenv(FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR); + } + + @VisibleForTesting + @Nullable + static boolean isEmulatorUrl(String databaseUrl) { + if (Strings.isNullOrEmpty(databaseUrl)) { + return false; + } + RepoInfo repoInfo = Utilities.parseUrl(databaseUrl).repoInfo; + return !repoInfo.host.endsWith(".firebaseio.com") && databaseUrl.contains("ns="); + } + + @Nullable + public static String getEmulatorUrl(String suppliedDatabaseUrl, String emulatorHost) { + if (isEmulatorUrl(suppliedDatabaseUrl)) { + return suppliedDatabaseUrl; + } + if (Strings.isNullOrEmpty(emulatorHost)) { + return null; + } + if (emulatorHost.contains("http:") || emulatorHost.contains("?ns=")) { + throw new IllegalArgumentException( + "emulator host declared in environment variable must be of the format \"host:port\""); + } + String namespaceName = "default"; + String path = "/"; + if (!Strings.isNullOrEmpty(suppliedDatabaseUrl)) { + ParsedUrl parsedDbUrl = Utilities.parseUrl(suppliedDatabaseUrl); + namespaceName = parsedDbUrl.repoInfo.namespace; + path = parsedDbUrl.path.isEmpty() ? "/" : parsedDbUrl.path.toString() + "/"; + } + // Must format correctly + return String.format("http://%s%s?ns=%s", emulatorHost, path, namespaceName); + } +} diff --git a/src/main/java/com/google/firebase/database/utilities/Utilities.java b/src/main/java/com/google/firebase/database/utilities/Utilities.java index 88c932e49..5a3c64822 100644 --- a/src/main/java/com/google/firebase/database/utilities/Utilities.java +++ b/src/main/java/com/google/firebase/database/utilities/Utilities.java @@ -18,78 +18,107 @@ import com.google.api.core.ApiFuture; import com.google.api.core.SettableApiFuture; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; +import com.google.common.base.Strings; import com.google.common.io.BaseEncoding; +import com.google.common.net.UrlEscapers; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseException; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.core.Path; import com.google.firebase.database.core.RepoInfo; - import java.io.UnsupportedEncodingException; import java.net.URI; -import java.net.URISyntaxException; +import java.net.URLDecoder; import java.net.URLEncoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.HashMap; import java.util.Map; +import org.apache.http.client.utils.URLEncodedUtils; public class Utilities { - private static final char[] HEX_CHARACTERS = "0123456789abcdef".toCharArray(); public static ParsedUrl parseUrl(String url) throws DatabaseException { - String original = url; try { - int schemeOffset = original.indexOf("//"); - if (schemeOffset == -1) { - throw new URISyntaxException(original, "Invalid scheme specified"); - } - int pathOffset = original.substring(schemeOffset + 2).indexOf("/"); - if (pathOffset != -1) { - pathOffset += schemeOffset + 2; - String[] pathSegments = original.substring(pathOffset).split("/"); - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < pathSegments.length; ++i) { - if (!pathSegments[i].equals("")) { - builder.append("/"); - builder.append(URLEncoder.encode(pathSegments[i], "UTF-8")); - } - } - original = original.substring(0, pathOffset) + builder.toString(); - } + URI uri = URI.create(url); - URI uri = new URI(original); - // URLEncoding a space turns it into a '+', which is different - // from our expected behavior. Do a manual replace to fix it. - final String pathString = uri.getPath().replace("+", " "); - Validation.validateRootPathString(pathString); String scheme = uri.getScheme(); + if (scheme == null) { + throw new IllegalArgumentException("Database URL does not specify a URL scheme"); + } + + String host = uri.getHost(); + if (host == null) { + throw new IllegalArgumentException("Database URL does not specify a valid host"); + } RepoInfo repoInfo = new RepoInfo(); - repoInfo.host = uri.getHost().toLowerCase(); + repoInfo.host = host.toLowerCase(); + repoInfo.secure = scheme.equals("https") || scheme.equals("wss"); int port = uri.getPort(); if (port != -1) { - repoInfo.secure = scheme.equals("https"); repoInfo.host += ":" + port; + } + + Map params = getQueryParamsMap(uri.getRawQuery()); + String namespaceParam = params.get("ns"); + if (!Strings.isNullOrEmpty(namespaceParam)) { + repoInfo.namespace = namespaceParam; } else { - repoInfo.secure = true; + String[] parts = host.split("\\.", -1); + repoInfo.namespace = parts[0].toLowerCase(); } - String[] parts = repoInfo.host.split("\\."); - repoInfo.namespace = parts[0].toLowerCase(); repoInfo.internalHost = repoInfo.host; + // use raw (encoded) path for backwards compatibility. + String pathString = uri.getRawPath(); + pathString = pathString.replace("+", " "); + Validation.validateRootPathString(pathString); + ParsedUrl parsedUrl = new ParsedUrl(); parsedUrl.path = new Path(pathString); parsedUrl.repoInfo = repoInfo; + return parsedUrl; + } catch (Exception e) { + throw new DatabaseException("Invalid Firebase Database url specified: " + url, e); + } + } - } catch (URISyntaxException e) { - throw new DatabaseException("Invalid Firebase Database url specified", e); - } catch (UnsupportedEncodingException e) { - throw new DatabaseException("Failed to URLEncode the path", e); + /** + * Extracts a map of query parameters from an encoded query string. Repeated parameters have + * values concatenated with commas. + * + * @param queryString to parse params from. Must be encoded. + * @return map of query parameters and their values. + */ + @VisibleForTesting + static Map getQueryParamsMap(String queryString) + throws UnsupportedEncodingException { + Map paramsMap = new HashMap<>(); + if (Strings.isNullOrEmpty(queryString)) { + return paramsMap; + } + String[] paramPairs = queryString.split("&"); + for (String paramPair : paramPairs) { + String[] pairParts = paramPair.split("="); + // both the first and second part will be encoded now, we must decode them + String decodedKey = URLDecoder.decode(pairParts[0], Charsets.UTF_8.name()); + String decodedValue = URLDecoder.decode(pairParts[1], Charsets.UTF_8.name()); + String runningValue = paramsMap.get(decodedKey); + if (Strings.isNullOrEmpty(runningValue)) { + runningValue = decodedValue; + } else { + runningValue += "," + decodedValue; + } + paramsMap.put(pairParts[0], runningValue); } + return paramsMap; } public static String[] splitIntoFrames(String src, int maxFrameSize) { diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java index c7036b4f0..ed3fb0631 100644 --- a/src/test/java/com/google/firebase/FirebaseAppTest.java +++ b/src/test/java/com/google/firebase/FirebaseAppTest.java @@ -35,6 +35,8 @@ import com.google.auth.oauth2.OAuth2Credentials.CredentialsChangedListener; import com.google.common.base.Defaults; import com.google.common.base.Strings; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp.TokenRefresher; @@ -580,6 +582,13 @@ public void testFirebaseExceptionEmptyDetail() { new FirebaseException(""); } + @Test + public void testFirebaseAppCreationWithEmptySupplier() { + FirebaseApp.initializeApp(FirebaseOptions.builder() + .setDatabaseUrl("https://test-ns.firebaseio.com") + .setCredentials(Suppliers.ofInstance(null)).build()); + } + private static void setFirebaseConfigEnvironmentVariable(String configJSON) { String configValue; if (configJSON.isEmpty() || configJSON.startsWith("{")) { diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java index 0623a3810..a3dac26fd 100644 --- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java +++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java @@ -156,12 +156,12 @@ public AccessToken refreshAccessToken() { @Test(expected = NullPointerException.class) public void createOptionsWithCredentialMissing() { - new FirebaseOptions.Builder().build(); + new FirebaseOptions.Builder().build().getCredentials(); } @Test(expected = NullPointerException.class) public void createOptionsWithNullCredentials() { - new FirebaseOptions.Builder().setCredentials(null).build(); + new FirebaseOptions.Builder().setCredentials((GoogleCredentials) null).build(); } @Test(expected = IllegalArgumentException.class) diff --git a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java index 6f2a3f7ec..74e835884 100644 --- a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java +++ b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java @@ -23,17 +23,17 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +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.database.util.EmulatorHelper; import com.google.firebase.testing.ServiceAccount; - import com.google.firebase.testing.TestUtils; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; - -import org.junit.AfterClass; -import org.junit.BeforeClass; +import java.util.List; import org.junit.Test; public class FirebaseDatabaseTest { @@ -44,100 +44,118 @@ public class FirebaseDatabaseTest { .setDatabaseUrl("https://firebase-db-test.firebaseio.com") .build(); - @BeforeClass - public static void setupClass() { - FirebaseApp.initializeApp(firebaseOptions); - } - - @AfterClass - public static void tearDownClass() { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); - } - @Test - public void testGetInstance() throws ExecutionException, InterruptedException { - FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance(); - assertNotNull(defaultDatabase); - assertSame(defaultDatabase, FirebaseDatabase.getInstance()); - assertSame(FirebaseApp.getInstance(), defaultDatabase.getApp()); + public void testGetInstance() { + FirebaseApp.initializeApp(firebaseOptions); + try { + FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance(); + assertNotNull(defaultDatabase); + assertSame(defaultDatabase, FirebaseDatabase.getInstance()); + assertSame(FirebaseApp.getInstance(), defaultDatabase.getApp()); + } finally { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } } @Test - public void testGetInstanceForUrl() throws ExecutionException, InterruptedException { - String url = "https://firebase-db-test2.firebaseio.com"; - FirebaseDatabase otherDatabase = FirebaseDatabase.getInstance(url); - assertNotNull(otherDatabase); - assertNotSame(otherDatabase, FirebaseDatabase.getInstance()); + public void testGetInstanceForUrl() { + FirebaseApp.initializeApp(firebaseOptions); + try { + String url = "https://firebase-db-test2.firebaseio.com"; + FirebaseDatabase otherDatabase = FirebaseDatabase.getInstance(url); + assertNotNull(otherDatabase); + assertNotSame(otherDatabase, FirebaseDatabase.getInstance()); + } finally { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } } @Test - public void testInvalidUrl() throws ExecutionException, InterruptedException { - String[] urls = new String[]{ - null, "", "https://firebase-db-test.firebaseio.com/foo" - }; - for (String url : urls) { - try { - FirebaseDatabase.getInstance(url); - fail("No error thrown for URL: " + url); - } catch (DatabaseException expected) { - // expected + public void testInvalidUrl() { + FirebaseApp.initializeApp(firebaseOptions); + try { + String[] urls = new String[]{ + null, "", "https://firebase-db-test.firebaseio.com/foo" + }; + for (String url : urls) { + try { + FirebaseDatabase.getInstance(url); + fail("No error thrown for URL: " + url); + } catch (DatabaseException expected) { + // expected + } } + } finally { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); } } @Test - public void testGetInstanceForApp() throws ExecutionException, InterruptedException { + public void testGetInstanceForApp() { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testGetInstanceForApp"); - FirebaseDatabase db = FirebaseDatabase.getInstance(app); - assertNotNull(db); - assertSame(db, FirebaseDatabase.getInstance(app)); + try { + FirebaseDatabase db = FirebaseDatabase.getInstance(app); + assertNotNull(db); + assertSame(db, FirebaseDatabase.getInstance(app)); + } finally { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } } @Test public void testReference() { - FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance(); - DatabaseReference reference = defaultDatabase.getReference(); - assertNotNull(reference); - assertNull(reference.getKey()); - assertNull(reference.getParent()); - - reference = defaultDatabase.getReference("foo"); - assertNotNull(reference); - assertEquals("foo", reference.getKey()); - assertNull(reference.getParent().getKey()); - - reference = defaultDatabase.getReference("foo/bar"); - assertNotNull(reference); - assertEquals("bar", reference.getKey()); - assertEquals("foo", reference.getParent().getKey()); + FirebaseApp.initializeApp(firebaseOptions); + try { + FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance(); + DatabaseReference reference = defaultDatabase.getReference(); + assertNotNull(reference); + assertNull(reference.getKey()); + assertNull(reference.getParent()); + + reference = defaultDatabase.getReference("foo"); + assertNotNull(reference); + assertEquals("foo", reference.getKey()); + assertNull(reference.getParent().getKey()); + + reference = defaultDatabase.getReference("foo/bar"); + assertNotNull(reference); + assertEquals("bar", reference.getKey()); + assertEquals("foo", reference.getParent().getKey()); + } finally { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } } @Test public void testReferenceFromUrl() { - FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance(); - DatabaseReference reference = defaultDatabase.getReferenceFromUrl( - "https://firebase-db-test.firebaseio.com/foo/bar"); - assertNotNull(reference); - assertEquals("bar", reference.getKey()); - assertEquals("foo", reference.getParent().getKey()); - + FirebaseApp.initializeApp(firebaseOptions); try { - defaultDatabase.getReferenceFromUrl(null); - fail("No error thrown for null URL"); - } catch (NullPointerException expected) { - // expected - } + FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance(); + DatabaseReference reference = defaultDatabase.getReferenceFromUrl( + "https://firebase-db-test.firebaseio.com/foo/bar"); + assertNotNull(reference); + assertEquals("bar", reference.getKey()); + assertEquals("foo", reference.getParent().getKey()); + try { + defaultDatabase.getReferenceFromUrl(null); + fail("No error thrown for null URL"); + } catch (NullPointerException expected) { + // expected + } - try { - defaultDatabase.getReferenceFromUrl("https://other-db-test.firebaseio.com/foo/bar"); - fail("No error thrown for invalid URL"); - } catch (DatabaseException expected) { - // expected + try { + defaultDatabase.getReferenceFromUrl("https://other-db-test.firebaseio.com/foo/bar"); + fail("No error thrown for invalid URL"); + } catch (DatabaseException expected) { + // expected + } + } finally { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); } } @Test - public void testAppDelete() throws ExecutionException, InterruptedException { + public void testAppDelete() { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testAppDelete"); FirebaseDatabase db = FirebaseDatabase.getInstance(app); assertNotNull(db); @@ -159,16 +177,119 @@ public void testAppDelete() throws ExecutionException, InterruptedException { } @Test - public void testInitAfterAppDelete() throws ExecutionException, InterruptedException, - TimeoutException { - FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testInitAfterAppDelete"); - FirebaseDatabase db1 = FirebaseDatabase.getInstance(app); - assertNotNull(db1); - app.delete(); + public void testInitAfterAppDelete() { + try { + FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testInitAfterAppDelete"); + FirebaseDatabase db1 = FirebaseDatabase.getInstance(app); + assertNotNull(db1); + app.delete(); + + app = FirebaseApp.initializeApp(firebaseOptions, "testInitAfterAppDelete"); + FirebaseDatabase db2 = FirebaseDatabase.getInstance(app); + assertNotNull(db2); + assertNotSame(db1, db2); + } finally { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + } + + @Test + public void testDbUrlIsEmulatorUrlWhenSettingOptionsManually() { + + List testCases = ImmutableList.of( + // cases where the env var is ignored because the supplied DB URL is a valid emulator URL + new CustomTestCase("http://my-custom-hosted-emulator.com:80?ns=dummy-ns", "", + "http://my-custom-hosted-emulator.com:80", "dummy-ns"), + new CustomTestCase("http://localhost:9000?ns=test-ns", null, + "http://localhost:9000", "test-ns"), + + // cases where the supplied DB URL is not an emulator URL, so we extract ns from it + // and append it to the emulator URL from env var(if it is valid) + new CustomTestCase("https://valid-namespace.firebaseio.com", "localhost:8080", + "http://localhost:8080", "valid-namespace"), + new CustomTestCase("https://test.firebaseio.com?ns=valid-namespace", "localhost:90", + "http://localhost:90", "valid-namespace") + ); + for (CustomTestCase tc : testCases) { + try { + FirebaseApp app = FirebaseApp.initializeApp(); + TestUtils.setEnvironmentVariables( + ImmutableMap.of(EmulatorHelper.FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR, + Strings.nullToEmpty(tc.envVariableUrl))); + FirebaseDatabase instance = FirebaseDatabase.getInstance(app, tc.rootDbUrl); + assertEquals(tc.expectedEmulatorRootUrl, + instance.getReference().repo.getRepoInfo().toString()); + assertEquals(tc.namespace, instance.getReference().repo.getRepoInfo().namespace); + // clean up after + app.delete(); + } finally { + TestUtils.unsetEnvironmentVariables( + ImmutableSet.of(EmulatorHelper.FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR)); + } + } + } + + @Test + public void testDbUrlIsEmulatorUrlForDbRefWithPath() { + + List testCases = ImmutableList.of( + new CustomTestCase("http://my-custom-hosted-emulator.com:80?ns=dummy-ns", + "http://my-custom-hosted-emulator.com:80?ns=dummy-ns", "", + "http://my-custom-hosted-emulator.com:80", "dummy-ns", "/"), + new CustomTestCase("http://localhost:9000?ns=test-ns", + "http://localhost:9000/a/b/c/d?ns=test-ns", null, + "http://localhost:9000", "test-ns", "/a/b/c/d"), + new CustomTestCase("http://localhost:9000?ns=test-ns", + "https://valid-namespace.firebaseio.com/a/b/c/d?ns=test-ns", "localhost:9000", + "http://localhost:9000", "test-ns", "/a/b/c/d"), + new CustomTestCase("https://valid-namespace.firebaseio.com", + "http://valid-namespace.firebaseio.com/a/b/c/d", "localhost:8080", + "http://localhost:8080", "valid-namespace", "/a/b/c/d") + ); + + for (CustomTestCase tc : testCases) { + try { + FirebaseApp app = FirebaseApp.initializeApp(); + TestUtils.setEnvironmentVariables( + ImmutableMap.of(EmulatorHelper.FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR, + Strings.nullToEmpty(tc.envVariableUrl))); + FirebaseDatabase instance = FirebaseDatabase.getInstance(app, tc.rootDbUrl); + DatabaseReference dbRef = instance.getReferenceFromUrl(tc.pathUrl); + assertEquals(tc.expectedEmulatorRootUrl, dbRef.repo.getRepoInfo().toString()); + assertEquals(tc.namespace, dbRef.repo.getRepoInfo().namespace); + assertEquals(tc.path, dbRef.path.toString()); + // clean up after + app.delete(); + + } finally { + TestUtils.unsetEnvironmentVariables( + ImmutableSet.of(EmulatorHelper.FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR)); + } + } + } + + private static class CustomTestCase { - app = FirebaseApp.initializeApp(firebaseOptions, "testInitAfterAppDelete"); - FirebaseDatabase db2 = FirebaseDatabase.getInstance(app); - assertNotNull(db2); - assertNotSame(db1, db2); + private String rootDbUrl; + private String pathUrl; + private String envVariableUrl; + private String expectedEmulatorRootUrl; + private String namespace; + private String path; + + private CustomTestCase(String rootDbUrl, String envVariableUrl, + String expectedEmulatorRootUrl, String namespace) { + this(rootDbUrl, null, envVariableUrl, expectedEmulatorRootUrl, namespace, null); + } + + private CustomTestCase(String rootDbUrl, String pathUrl, String envVariableUrl, + String expectedEmulatorRootUrl, String namespace, String path) { + this.rootDbUrl = rootDbUrl; + this.pathUrl = pathUrl; + this.envVariableUrl = envVariableUrl; + this.expectedEmulatorRootUrl = expectedEmulatorRootUrl; + this.namespace = namespace; + this.path = path; + } } } diff --git a/src/test/java/com/google/firebase/database/util/EmulatorHelperTest.java b/src/test/java/com/google/firebase/database/util/EmulatorHelperTest.java new file mode 100644 index 000000000..bffd8a5e3 --- /dev/null +++ b/src/test/java/com/google/firebase/database/util/EmulatorHelperTest.java @@ -0,0 +1,134 @@ +package com.google.firebase.database.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.database.DatabaseException; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class EmulatorHelperTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testExtractingEmulatorUrlFromSuppliedUrlSucceeds() { + Map suppliedToExpectedUrlsMap = ImmutableMap.of( + "http://localhost:9000?ns=test-ns", "http://localhost:9000/?ns=test-ns", + "http://my-custom-hosted-emulator.com:80?ns=dummy-ns", + "http://my-custom-hosted-emulator.com:80/?ns=dummy-ns", + "http://my-custom-hosted-emulator.com:80/path/to/document?ns=dummy-ns", + "http://my-custom-hosted-emulator.com:80/path/to/document/?ns=dummy-ns", + "https://localhost:9000?ns=test-ns", "https://localhost:9000/?ns=test-ns", + "http://my-custom-hosted-emulator.com?ns=dummy-ns", + "http://my-custom-hosted-emulator.com/?ns=dummy-ns" + ); + for (Map.Entry e : suppliedToExpectedUrlsMap.entrySet()) { + assertTrue(EmulatorHelper.isEmulatorUrl(e.getKey())); + } + } + + @Test + public void testExtractingEmulatorUrlsFromSuppliedUrlFails() { + List nonEmulatorUrls = ImmutableList.of( + "http://localhost:9000",// missing ns param + "http://my-custom-hosted-emulator.com:80", // missing ns param + "http://test-namespace.firebaseio.com" // firebaseio.com not supported + ); + for (String url : nonEmulatorUrls) { + assertFalse(EmulatorHelper.isEmulatorUrl(url)); + } + } + + @Test + public void testExtractingEmulatorUrlsThrowsException() { + List invalidFormedUrls = ImmutableList.of( + "localhost", + "localhost:9000" + ); + for (String invalidFormedUrl : invalidFormedUrls) { + thrown.expect(DatabaseException.class); + EmulatorHelper.isEmulatorUrl(invalidFormedUrl); + } + } + + @Test + public void testEmulatorUrlCorrectlyPickedUp() { + class CustomTestCase { + + private String suppliedDbUrl; + private String envVariableUrl; + private String expectedEmulatorUrl; + + private CustomTestCase(String suppliedDbUrl, String envVariableUrl, + String expectedEmulatorUrl) { + this.suppliedDbUrl = suppliedDbUrl; + this.envVariableUrl = envVariableUrl; + this.expectedEmulatorUrl = expectedEmulatorUrl; + } + } + + List testCases; // separated declaration and assignment coz of checkstyle + testCases = ImmutableList.of( + // cases where the env var is ignored because the supplied DB URL is a valid emulator URL + new CustomTestCase("http://my-custom-hosted-emulator.com:80?ns=dummy-ns", "", + "http://my-custom-hosted-emulator.com:80?ns=dummy-ns"), + new CustomTestCase("http://localhost:9000?ns=test-ns", null, + "http://localhost:9000?ns=test-ns"), + new CustomTestCase("http://my-custom-hosted-emulator.com:80?ns=dummy-ns", + "http://localhost:8080?ns=ns-2", + "http://my-custom-hosted-emulator.com:80?ns=dummy-ns"), + new CustomTestCase("http://localhost:9000?ns=ns-1", "localhost:8080", + "http://localhost:9000?ns=ns-1"), + new CustomTestCase("http://localhost:9000?ns=ns-1", "http://localhost:8080/ns=ns-2", + "http://localhost:9000?ns=ns-1"), + new CustomTestCase("http://localhost:9000/a/b/c?ns=ns-1", "http://localhost:8080/ns=ns-2", + "http://localhost:9000/a/b/c?ns=ns-1"), + new CustomTestCase("https://firebaseio.com?ns=valid-namespace", "localhost:90", + "https://firebaseio.com?ns=valid-namespace"), + + // cases where the supplied DB URL is not an emulator URL, so we extract ns from it + // and append it to the emulator URL from env var(if it is valid) + new CustomTestCase("https://valid-namespace.firebaseio.com", "localhost:8080", + "http://localhost:8080/?ns=valid-namespace"), + new CustomTestCase("https://valid-namespace.firebaseio.com", "custom-emulator-url:90", + "http://custom-emulator-url:90/?ns=valid-namespace"), + new CustomTestCase("https://valid-namespace.firebaseio.com/a/b/c", "custom-emulator-url:90", + "http://custom-emulator-url:90/a/b/c/?ns=valid-namespace"), + new CustomTestCase("https://valid-namespace.firebaseio.com", "192.123.212.145:90", + "http://192.123.212.145:90/?ns=valid-namespace"), + new CustomTestCase("https://valid-namespace.firebaseio.com", "[::1]:90", + "http://[::1]:90/?ns=valid-namespace"), + new CustomTestCase(null, "localhost:90", "http://localhost:90/?ns=default"), + new CustomTestCase("", "localhost:90", "http://localhost:90/?ns=default") + ); + + for (CustomTestCase tc : testCases) { + assertEquals(tc.expectedEmulatorUrl, + EmulatorHelper.getEmulatorUrl(tc.suppliedDbUrl, tc.envVariableUrl)); + } + } + + @Test + public void testInvalidEmulatorUrlFromEnvVarThrows() { + List invalidEnvVars = ImmutableList.of( + "http://localhost:8080", + "http://localhost:8080?ns=test-ns", + "localhost" + ); + for (String invalidEnvVar : invalidEnvVars) { + thrown.expect(IllegalArgumentException.class); + EmulatorHelper.getEmulatorUrl("https://valid-namespace.firebaseio.com", + invalidEnvVar); + } + } +} + + diff --git a/src/test/java/com/google/firebase/database/utilities/UtilitiesTest.java b/src/test/java/com/google/firebase/database/utilities/UtilitiesTest.java index f4efbad57..0dcc1cbd0 100644 --- a/src/test/java/com/google/firebase/database/utilities/UtilitiesTest.java +++ b/src/test/java/com/google/firebase/database/utilities/UtilitiesTest.java @@ -32,6 +32,9 @@ import com.google.firebase.database.DatabaseException; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.DatabaseReference.CompletionListener; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.util.Map; import java.util.concurrent.ExecutionException; import org.junit.Test; @@ -74,6 +77,15 @@ public void testParseValidUrl() { assertTrue(url.repoInfo.isSecure()); assertEquals("test", url.repoInfo.namespace); assertEquals(ImmutableList.of("foo", "bar"), url.path.asList()); + + url = Utilities + .parseUrl("https://firebaseio.com/path%20with%20spaces/?ns=random%20valid%20namespace"); + assertTrue(url.repoInfo.isSecure()); + assertEquals("random valid namespace", url.repoInfo.namespace); + assertEquals("/path%20with%20spaces", url.path.toString()); + + url = Utilities.parseUrl("http://test.firebaseio.com/+"); + assertEquals("/ ", url.path.toString()); } @Test @@ -185,4 +197,21 @@ public void onComplete(DatabaseError error, DatabaseReference ref) { assertSame(listener, result.getSecond()); } + @Test + public void testExtractParamsFromUrl() throws UnsupportedEncodingException { + Map params = Utilities.getQueryParamsMap("abc=213&qpf=2312&xyz=true&qpf=hi"); + assertEquals("213", params.get("abc")); + assertEquals("2312,hi", params.get("qpf")); + assertEquals("true", params.get("xyz")); + + params = Utilities.getQueryParamsMap( + "q=a%3D2%26b%3D3&oq=a%3D2%26b%3D3&aqs=chrome..69i57j0l5.4023j0j7&sourceid=chrome&ie=UTF-8"); + assertEquals("a=2&b=3", params.get("q")); + assertEquals("a=2&b=3", params.get("oq")); + assertEquals("chrome", params.get("sourceid")); + + params = Utilities.getQueryParamsMap("a=%3F%3F%3F&b=%3D%26%3D"); + assertEquals("???", params.get("a")); + assertEquals("=&=", params.get("b")); + } } diff --git a/src/test/java/com/google/firebase/testing/TestUtils.java b/src/test/java/com/google/firebase/testing/TestUtils.java index f8e1b4d0e..a63aa8d10 100644 --- a/src/test/java/com/google/firebase/testing/TestUtils.java +++ b/src/test/java/com/google/firebase/testing/TestUtils.java @@ -43,6 +43,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** Test Utils for use by all tests (both unit and integration tests). */ public class TestUtils { @@ -85,6 +86,30 @@ public static void setEnvironmentVariables(Map vars) { } } + public static void unsetEnvironmentVariables(Set vars) { + // Unsetting the environment variables after the JVM has started requires a bit of a hack: + // we reach into the package-private java.lang.ProcessEnvironment class, which incidentally + // is platform-specific, and replace the map held in a static final field there, + // using yet more reflection. + // + // This is copied from {#see com.google.apphosting.runtime.NullSandboxPlugin} + Map allVars = new HashMap<>(System.getenv()); + for (String var : vars) { + allVars.remove(var); + } + try { + Class pe = Class.forName("java.lang.ProcessEnvironment", true, null); + Field f = pe.getDeclaredField("theUnmodifiableEnvironment"); + f.setAccessible(true); + Field m = Field.class.getDeclaredField("modifiers"); + m.setAccessible(true); + m.setInt(f, m.getInt(f) & ~Modifier.FINAL); + f.set(null, Collections.unmodifiableMap(allVars)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("failed to unset the environment variables", e); + } + } + public static String loadResource(String path) { InputStream stream = TestUtils.class.getClassLoader().getResourceAsStream(path); checkNotNull(stream, "Failed to load resource: %s", path); From fab1435c5c0864b0495e4f6f40c25792e1751bea Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 26 Aug 2019 10:20:19 -0700 Subject: [PATCH 075/456] Removing incorrectly applied Nullable annotation (#302) --- src/main/java/com/google/firebase/FirebaseApp.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/google/firebase/FirebaseApp.java b/src/main/java/com/google/firebase/FirebaseApp.java index f69ee36c9..c60f0ad5e 100644 --- a/src/main/java/com/google/firebase/FirebaseApp.java +++ b/src/main/java/com/google/firebase/FirebaseApp.java @@ -132,7 +132,6 @@ public static List getApps() { * * @throws IllegalStateException if the default app was not initialized. */ - @Nullable public static FirebaseApp getInstance() { return getInstance(DEFAULT_APP_NAME); } From cfb1989ab217701394880989c2333aa9312ac2c9 Mon Sep 17 00:00:00 2001 From: chong-shao <31256040+chong-shao@users.noreply.github.com> Date: Tue, 27 Aug 2019 16:42:36 -0700 Subject: [PATCH 076/456] feat(fcm): Add image in notification support. (#298) * Add image in notification support. * Fix indentation * change param name from image to imageUrl * Fix minor issues * fix tests --- .../messaging/AndroidNotification.java | 17 +++++ .../firebase/messaging/ApnsFcmOptions.java | 15 ++++ .../firebase/messaging/Notification.java | 15 ++++ .../messaging/FirebaseMessagingIT.java | 3 +- .../firebase/messaging/MessageTest.java | 74 +++++++++++++++++++ 5 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index 064f493c2..ef286d499 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -66,6 +66,9 @@ public class AndroidNotification { @Key("channel_id") private final String channelId; + + @Key("image") + private final String image; private AndroidNotification(Builder builder) { this.title = builder.title; @@ -97,6 +100,7 @@ private AndroidNotification(Builder builder) { this.titleLocArgs = null; } this.channelId = builder.channelId; + this.image = builder.image; } /** @@ -122,6 +126,7 @@ public static class Builder { private String titleLocKey; private List titleLocArgs = new ArrayList<>(); private String channelId; + private String image; private Builder() {} @@ -292,6 +297,18 @@ public Builder setChannelId(String channelId) { return this; } + /** + * Sets the URL of the image that is going to be displayed in the notification. When provided, + * overrides the imageUrl set via {@link Notification}. + * + * @param imageUrl URL of the image that is going to be displayed in the notification. + * @return This builder. + */ + public Builder setImage(String imageUrl) { + this.image = imageUrl; + return this; + } + /** * Creates a new {@link AndroidNotification} instance from the parameters set on this builder. * diff --git a/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java b/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java index 708c0b56d..46bc5702f 100644 --- a/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java +++ b/src/main/java/com/google/firebase/messaging/ApnsFcmOptions.java @@ -24,10 +24,14 @@ public final class ApnsFcmOptions { @Key("analytics_label") private final String analyticsLabel; + + @Key("image") + private final String image; private ApnsFcmOptions(Builder builder) { FcmOptionsUtil.checkAnalyticsLabel(builder.analyticsLabel); this.analyticsLabel = builder.analyticsLabel; + this.image = builder.image; } /** @@ -53,6 +57,8 @@ public static class Builder { private String analyticsLabel; + private String image; + private Builder() {} /** @@ -64,6 +70,15 @@ public Builder setAnalyticsLabel(String analyticsLabel) { return this; } + /** + * @param imageUrl URL of the image that is going to be displayed in the notification. + * @return This builder + */ + public Builder setImage(String imageUrl) { + this.image = imageUrl; + return this; + } + /** * Creates a new {@link ApnsFcmOptions} instance from the parameters set on this builder. * diff --git a/src/main/java/com/google/firebase/messaging/Notification.java b/src/main/java/com/google/firebase/messaging/Notification.java index eaa1e1443..866c06680 100644 --- a/src/main/java/com/google/firebase/messaging/Notification.java +++ b/src/main/java/com/google/firebase/messaging/Notification.java @@ -29,6 +29,9 @@ public class Notification { @Key("body") private final String body; + + @Key("image") + private final String image; /** * Creates a new {@code Notification} using the given title and body. @@ -37,8 +40,20 @@ public class Notification { * @param body Body of the notification. */ public Notification(String title, String body) { + this(title, body, null); + } + + /** + * Creates a new {@code Notification} using the given title, body, and image. + * + * @param title Title of the notification. + * @param body Body of the notification. + * @param imageUrl URL of the image that is going to be displayed in the notification. + */ + public Notification(String title, String body, String imageUrl) { this.title = title; this.body = body; + this.image = imageUrl; } } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 751ba8bdd..494923f90 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -34,6 +34,7 @@ public class FirebaseMessagingIT { private static final String TEST_REGISTRATION_TOKEN = "fGw0qy4TGgk:APA91bGtWGjuhp4WRhHXgbabIYp1jxEKI08ofj_v1bKhWAGJQ4e3arRCWzeTfHaLz83mBnDh0a" + "PWB1AykXAVUUGl2h1wT4XI6XazWpvY7RBUSYfoxtqSWGIm2nvWh2BOP1YG501SsRoE"; + private static final String TEST_IMAGE_URL = "https://example.com/image.png"; @BeforeClass public static void setUpClass() { @@ -44,7 +45,7 @@ public static void setUpClass() { public void testSend() throws Exception { FirebaseMessaging messaging = FirebaseMessaging.getInstance(); Message message = Message.builder() - .setNotification(new Notification("Title", "Body")) + .setNotification(new Notification("Title", "Body", TEST_IMAGE_URL)) .setAndroidConfig(AndroidConfig.builder() .setRestrictedPackageName("com.google.firebase.testing") .build()) diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 6744a35f6..2db28d5dc 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -36,6 +36,10 @@ public class MessageTest { + private static final String TEST_IMAGE_URL = "https://example.com/image.png"; + private static final String TEST_IMAGE_URL_ANDROID = "https://example.com/android-image.png"; + private static final String TEST_IMAGE_URL_APNS = "https://example.com/apns-image.png"; + @Test(expected = IllegalArgumentException.class) public void testMessageWithoutTarget() { Message.builder().build(); @@ -698,6 +702,76 @@ public void testIncorrectAnalyticsLabelFormat() { } } + @Test + public void testImageInNotification() throws IOException { + Message message = Message.builder() + .setNotification(new Notification("title", "body", TEST_IMAGE_URL)) + .setTopic("test-topic") + .build(); + Map data = ImmutableMap.of( + "title", "title", "body", "body", "image", TEST_IMAGE_URL); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "notification", data), message); + } + + @Test + public void testImageInAndroidNotification() throws IOException { + Message message = Message.builder() + .setNotification(new Notification("title", "body", TEST_IMAGE_URL)) + .setAndroidConfig(AndroidConfig.builder() + .setNotification(AndroidNotification.builder() + .setTitle("android-title") + .setBody("android-body") + .setImage(TEST_IMAGE_URL_ANDROID) + .build()) + .build()) + .setTopic("test-topic") + .build(); + Map notification = ImmutableMap.builder() + .put("title", "title") + .put("body", "body") + .put("image", TEST_IMAGE_URL) + .build(); + Map androidConfig = ImmutableMap.builder() + .put("notification", ImmutableMap.builder() + .put("title", "android-title") + .put("body", "android-body") + .put("image", TEST_IMAGE_URL_ANDROID) + .build()) + .build(); + assertJsonEquals(ImmutableMap.of( + "topic", "test-topic", "notification", notification, "android", androidConfig), message); + } + + @Test + public void testImageInApnsNotification() throws IOException { + Message message = Message.builder() + .setTopic("test-topic") + .setNotification(new Notification("title", "body", TEST_IMAGE_URL)) + .setApnsConfig( + ApnsConfig.builder().setAps(Aps.builder().build()) + .setFcmOptions(ApnsFcmOptions.builder().setImage(TEST_IMAGE_URL_APNS).build()) + .build()).build(); + + ImmutableMap notification = + ImmutableMap.builder() + .put("title", "title") + .put("body", "body") + .put("image", TEST_IMAGE_URL) + .build(); + ImmutableMap apnsConfig = + ImmutableMap.builder() + .put("fcm_options", ImmutableMap.of("image", TEST_IMAGE_URL_APNS)) + .put("payload", ImmutableMap.of("aps", ImmutableMap.of())) + .build(); + ImmutableMap expected = + ImmutableMap.builder() + .put("topic", "test-topic") + .put("notification", notification) + .put("apns", apnsConfig) + .build(); + assertJsonEquals(expected, message); + } + private static void assertJsonEquals( Map expected, Object actual) throws IOException { assertEquals(expected, toMap(actual)); From c6f8f9720c40eade7f8c136a012a2f96586a09aa Mon Sep 17 00:00:00 2001 From: egilmorez Date: Tue, 3 Sep 2019 10:20:31 -0700 Subject: [PATCH 077/456] Update to fix broken link (#303) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b60aba82c..467c0d377 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ We get lots of those and we love helping you, but GitHub is not the best place f which just ask about usage will be closed. Here are some resources to get help: - Go through the [guides](https://firebase.google.com/docs/admin/setup/) -- Read the full [API reference](https://firebase.google.com/docs/reference/admin/java/) +- Read the full [API reference](https://firebase.google.com/docs/reference/admin#java) If the official documentation doesn't help, try asking a question on the [Firebase Google Group](https://groups.google.com/forum/#!forum/firebase-talk/) or one of our From d5b2d45538c527ae57894831d1726f762047898e Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Tue, 3 Sep 2019 14:06:37 -0700 Subject: [PATCH 078/456] Fix emulator env var name. (#305) --- .../java/com/google/firebase/database/util/EmulatorHelper.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/database/util/EmulatorHelper.java b/src/main/java/com/google/firebase/database/util/EmulatorHelper.java index 833f9760e..c4ef2ff25 100644 --- a/src/main/java/com/google/firebase/database/util/EmulatorHelper.java +++ b/src/main/java/com/google/firebase/database/util/EmulatorHelper.java @@ -29,7 +29,8 @@ private EmulatorHelper() { } @VisibleForTesting - public static final String FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR = "FIREBASE_RTDB_EMULATOR_HOST"; + public static final String FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR = + "FIREBASE_DATABASE_EMULATOR_HOST"; public static String getEmulatorHostFromEnv() { return System.getenv(FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR); From dbe350426e9076113a902140a4b42cc8fb1626d2 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 4 Sep 2019 13:35:51 -0700 Subject: [PATCH 079/456] Fixing an intermittent test failure (#306) --- src/test/java/com/google/firebase/FirebaseAppTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java index ed3fb0631..eee9efdb8 100644 --- a/src/test/java/com/google/firebase/FirebaseAppTest.java +++ b/src/test/java/com/google/firebase/FirebaseAppTest.java @@ -39,6 +39,7 @@ import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.firebase.FirebaseApp.TokenRefresher; import com.google.firebase.FirebaseOptions.Builder; import com.google.firebase.database.FirebaseDatabase; @@ -60,6 +61,7 @@ import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Rule; @@ -83,6 +85,11 @@ public static void setupClass() throws IOException { TestUtils.getApplicationDefaultCredentials(); } + @AfterClass + public static void tearDownClass() { + TestUtils.unsetEnvironmentVariables(ImmutableSet.of(FirebaseApp.FIREBASE_CONFIG_ENV_VAR)); + } + @Test(expected = NullPointerException.class) public void testNullAppName() { FirebaseApp.initializeApp(OPTIONS, null); From d8c611153cd92a6cf872b232667d65977113a13e Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 4 Sep 2019 14:03:23 -0700 Subject: [PATCH 080/456] Staged Release 6.10.0 (#307) * Updating CHANGELOG for 6.10.0 release. * [maven-release-plugin] prepare release v6.10.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 d95bd8e71..ddc7afb4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +- + +# v6.10.0 + - [added] Added `WebpushFcmOptions` to the `FirebaseMessaging` API, providing the `setLink()` method. diff --git a/pom.xml b/pom.xml index c16717c11..f8fbb717f 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.9.1-SNAPSHOT + 6.10.1-SNAPSHOT jar firebase-admin From d6cda02d65b274bd64fd01381ef1e2f6809748d9 Mon Sep 17 00:00:00 2001 From: chong-shao <31256040+chong-shao@users.noreply.github.com> Date: Wed, 11 Sep 2019 14:46:42 -0700 Subject: [PATCH 081/456] feat(fcm): Added a `Builder` for constructing `Notification` objects (#311) * Add a builder for Notification * Adjust the integration test --- .../firebase/messaging/Notification.java | 73 ++++++++++++++++++- .../messaging/FirebaseMessagingIT.java | 6 +- .../firebase/messaging/MessageTest.java | 29 +++++++- 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/Notification.java b/src/main/java/com/google/firebase/messaging/Notification.java index 866c06680..d9b2034ee 100644 --- a/src/main/java/com/google/firebase/messaging/Notification.java +++ b/src/main/java/com/google/firebase/messaging/Notification.java @@ -38,17 +38,21 @@ public class Notification { * * @param title Title of the notification. * @param body Body of the notification. + * + * @deprecated Use {@link #Notification(Builder)} instead. */ public Notification(String title, String body) { this(title, body, null); } - + /** * Creates a new {@code Notification} using the given title, body, and image. * * @param title Title of the notification. * @param body Body of the notification. * @param imageUrl URL of the image that is going to be displayed in the notification. + * + * @deprecated Use {@link #Notification(Builder)} instead. */ public Notification(String title, String body, String imageUrl) { this.title = title; @@ -56,4 +60,71 @@ public Notification(String title, String body, String imageUrl) { this.image = imageUrl; } + private Notification(Builder builder) { + this.title = builder.title; + this.body = builder.body; + this.image = builder.image; + } + + /** + * Creates a new {@link Notification.Builder}. + * + * @return A {@link Notification.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String title; + private String body; + private String image; + + private Builder() {} + + /** + * Sets the title of the notification. + * + * @param title Title of the notification. + * @return This builder. + */ + public Builder setTitle(String title) { + this.title = title; + return this; + } + + /** + * Sets the body of the notification. + * + * @param body Body of the notification. + * @return This builder. + */ + public Builder setBody(String body) { + this.body = body; + return this; + } + + /** + * Sets the URL of the image that is going to be displayed in the notification. + * + * @param imageUrl URL of the image that is going to be displayed in the notification. + * @return This builder. + */ + public Builder setImage(String imageUrl) { + this.image = imageUrl; + return this; + } + + /** + * Creates a new {@link Notification} instance from the parameters set on this builder. + * + * @return A new {@link Notification} instance. + * @throws IllegalArgumentException If any of the parameters set on the builder are invalid. + */ + public Notification build() { + return new Notification(this); + } + } + } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 494923f90..8951d7cd8 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -45,7 +45,11 @@ public static void setUpClass() { public void testSend() throws Exception { FirebaseMessaging messaging = FirebaseMessaging.getInstance(); Message message = Message.builder() - .setNotification(new Notification("Title", "Body", TEST_IMAGE_URL)) + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .setImage(TEST_IMAGE_URL) + .build()) .setAndroidConfig(AndroidConfig.builder() .setRestrictedPackageName("com.google.firebase.testing") .build()) diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 2db28d5dc..556f2d9ea 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -88,7 +88,7 @@ public void testPrefixedTopicName() throws IOException { } @Test - public void testNotificationMessage() throws IOException { + public void testNotificationMessageDeprecatedConstructor() throws IOException { Message message = Message.builder() .setNotification(new Notification("title", "body")) .setTopic("test-topic") @@ -97,6 +97,16 @@ public void testNotificationMessage() throws IOException { assertJsonEquals(ImmutableMap.of("topic", "test-topic", "notification", data), message); } + @Test + public void testNotificationMessage() throws IOException { + Message message = Message.builder() + .setNotification(Notification.builder().setTitle("title").setBody("body").build()) + .setTopic("test-topic") + .build(); + Map data = ImmutableMap.of("title", "title", "body", "body"); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "notification", data), message); + } + @Test public void testEmptyAndroidMessage() throws IOException { Message message = Message.builder() @@ -703,7 +713,7 @@ public void testIncorrectAnalyticsLabelFormat() { } @Test - public void testImageInNotification() throws IOException { + public void testImageInNotificationDeprecatedConstructor() throws IOException { Message message = Message.builder() .setNotification(new Notification("title", "body", TEST_IMAGE_URL)) .setTopic("test-topic") @@ -713,6 +723,21 @@ public void testImageInNotification() throws IOException { assertJsonEquals(ImmutableMap.of("topic", "test-topic", "notification", data), message); } + @Test + public void testImageInNotification() throws IOException { + Message message = Message.builder() + .setNotification(Notification.builder() + .setTitle("title") + .setBody("body") + .setImage(TEST_IMAGE_URL) + .build()) + .setTopic("test-topic") + .build(); + Map data = ImmutableMap.of( + "title", "title", "body", "body", "image", TEST_IMAGE_URL); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "notification", data), message); + } + @Test public void testImageInAndroidNotification() throws IOException { Message message = Message.builder() From d01b0626b42077448b1919fa41f0ddde85f7566b Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 13 Sep 2019 16:48:25 -0700 Subject: [PATCH 082/456] Upgraded Firestore and GCS (#312) --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index f8fbb717f..cffff27c4 100644 --- a/pom.xml +++ b/pom.xml @@ -412,17 +412,17 @@ com.google.auth google-auth-library-oauth2-http - 0.16.1 + 0.17.1 com.google.cloud google-cloud-storage - 1.79.0 + 1.91.0 com.google.cloud google-cloud-firestore - 1.9.0 + 1.21.0 From 87ce0ed73339420c047a18462cd2592c5cc8b371 Mon Sep 17 00:00:00 2001 From: nrsim <55852299+nrsim@users.noreply.github.com> Date: Wed, 2 Oct 2019 13:19:05 -0400 Subject: [PATCH 083/456] Remove (base64) 'REDACTED' passwords from user records. (#314) --- CONTRIBUTING.md | 10 ++++-- .../firebase/auth/ExportedUserRecord.java | 10 +++++- .../google/firebase/auth/FirebaseAuthIT.java | 4 ++- .../firebase/auth/ListUsersPageTest.java | 36 +++++++++++++++++++ 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 467c0d377..11012bfff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -136,9 +136,13 @@ Create a new project in the [Firebase console](https://console.firebase.google.c not already have one. Use a separate, dedicated project for integration tests since the test suite makes a large number of writes to the Firebase realtime database. Download the service account private key from the "Settings > Service Accounts" page of the project, and save it as -`integration_cert.json` at the root of the codebase. Also obtain the web API key of the project -from the "Settings > General" page, and save it as `integration_apikey.txt` at the root of the -codebase. Now run the following command to invoke the integration test suite: +`integration_cert.json` at the root of the codebase. Grant your service account the `Firebase +Authentication Admin` role at +[Google Cloud Platform Console / IAM & admin](https://console.cloud.google.com/iam-admin). This is +required to ensure that exported user records contain the password hashes of the user accounts. +Also obtain the web API key of the project from the "Settings > General" page, and save it as +`integration_apikey.txt` at the root of the codebase. Now run the following command to invoke the +integration test suite: ``` mvn verify diff --git a/src/main/java/com/google/firebase/auth/ExportedUserRecord.java b/src/main/java/com/google/firebase/auth/ExportedUserRecord.java index e367f55ae..9fdfc9c5e 100644 --- a/src/main/java/com/google/firebase/auth/ExportedUserRecord.java +++ b/src/main/java/com/google/firebase/auth/ExportedUserRecord.java @@ -17,6 +17,7 @@ package com.google.firebase.auth; import com.google.api.client.json.JsonFactory; +import com.google.common.io.BaseEncoding; import com.google.firebase.auth.internal.DownloadAccountResponse.User; import com.google.firebase.internal.Nullable; @@ -28,10 +29,17 @@ public class ExportedUserRecord extends UserRecord { private final String passwordHash; private final String passwordSalt; + private static final String REDACTED_BASE64 = BaseEncoding.base64Url().encode( + "REDACTED".getBytes()); ExportedUserRecord(User response, JsonFactory jsonFactory) { super(response, jsonFactory); - this.passwordHash = response.getPasswordHash(); + String passwordHash = response.getPasswordHash(); + if (passwordHash != null && !passwordHash.equals(REDACTED_BASE64)) { + this.passwordHash = passwordHash; + } else { + this.passwordHash = null; + } this.passwordSalt = response.getPasswordSalt(); } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index badb9ead6..6a8361d0d 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -264,7 +264,9 @@ public void testListUsers() throws Exception { for (ExportedUserRecord user : page.getValues()) { if (uids.contains(user.getUid())) { collected.incrementAndGet(); - assertNotNull(user.getPasswordHash()); + assertNotNull("Missing passwordHash field. A common cause would be " + + "forgetting to add the \"Firebase Authentication Admin\" permission. See " + + "instructions in CONTRIBUTING.md", user.getPasswordHash()); assertNotNull(user.getPasswordSalt()); } } diff --git a/src/test/java/com/google/firebase/auth/ListUsersPageTest.java b/src/test/java/com/google/firebase/auth/ListUsersPageTest.java index 1c6282fa2..fb4a7d275 100644 --- a/src/test/java/com/google/firebase/auth/ListUsersPageTest.java +++ b/src/test/java/com/google/firebase/auth/ListUsersPageTest.java @@ -26,6 +26,7 @@ import com.google.api.client.googleapis.util.Utils; import com.google.api.client.json.JsonFactory; import com.google.common.collect.ImmutableList; +import com.google.common.io.BaseEncoding; import com.google.firebase.auth.ListUsersPage.ListUsersResult; import com.google.firebase.auth.internal.DownloadAccountResponse; import java.io.IOException; @@ -38,6 +39,9 @@ public class ListUsersPageTest { + private static final String REDACTED_BASE64 = BaseEncoding.base64Url().encode( + "REDACTED".getBytes()); + @Test public void testSinglePage() throws FirebaseAuthException, IOException { TestUserSource source = new TestUserSource(3); @@ -55,6 +59,30 @@ public void testSinglePage() throws FirebaseAuthException, IOException { assertNull(source.calls.get(0)); } + @Test + public void testRedactedPasswords() throws FirebaseAuthException, IOException { + ListUsersResult result = new ListUsersResult( + ImmutableList.of( + newUser("user0", REDACTED_BASE64), + newUser("user1", REDACTED_BASE64), + newUser("user2", REDACTED_BASE64)), + ListUsersPage.END_OF_LIST); + TestUserSource source = new TestUserSource(result); + ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + + ImmutableList users = ImmutableList.copyOf(page.getValues()); + assertEquals(3, users.size()); + for (int i = 0; i < 3; i++) { + assertEquals("user" + i, users.get(i).getUid()); + assertNull(users.get(i).getPasswordHash()); + } + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + @Test public void testMultiplePages() throws FirebaseAuthException, IOException { ListUsersResult result = new ListUsersResult( @@ -326,6 +354,14 @@ private static ExportedUserRecord newUser(String uid) throws IOException { return new ExportedUserRecord(parsed, jsonFactory); } + private static ExportedUserRecord newUser(String uid, String passwordHash) throws IOException { + JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + DownloadAccountResponse.User parsed = jsonFactory.fromString( + String.format("{\"localId\":\"%s\", \"passwordHash\":\"%s\"}", uid, passwordHash), + DownloadAccountResponse.User.class); + return new ExportedUserRecord(parsed, jsonFactory); + } + private static class TestUserSource implements ListUsersPage.UserSource { private ListUsersResult result; From 1bfb5845a7c4ec6d2d395da76bef20601c659784 Mon Sep 17 00:00:00 2001 From: rsgowman Date: Thu, 10 Oct 2019 17:37:12 -0400 Subject: [PATCH 084/456] Minor fix to docs (broken link: GoogleCredentials) (#318) b/140244055 --- src/main/java/com/google/firebase/FirebaseOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java index b93d92f32..f0561d5e5 100644 --- a/src/main/java/com/google/firebase/FirebaseOptions.java +++ b/src/main/java/com/google/firebase/FirebaseOptions.java @@ -316,7 +316,7 @@ public Builder setStorageBucket(String storageBucket) { * Initialize the SDK for code samples and detailed documentation. * * @param credentials A - * {@code GoogleCredentials} + * {@code GoogleCredentials} * instance used to authenticate the SDK. * @return This Builder instance is returned so subsequent calls can be chained. */ From d79485dfb67e3c721c34ec043847f7665af1a634 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 17 Oct 2019 13:08:32 -0700 Subject: [PATCH 085/456] Removed CHANGELOG file (#319) --- CHANGELOG.md | 512 --------------------------------------------- prepare_release.sh | 14 -- 2 files changed, 526 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index ddc7afb4f..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,512 +0,0 @@ -# Unreleased - -- - -# v6.10.0 - -- [added] Added `WebpushFcmOptions` to the `FirebaseMessaging` API, providing - the `setLink()` method. - -# v6.9.0 - -- [added] Added `FcmOptions`, `ApnsFcmOptions` and `AndroidFcmOptions` to the - `FirebaseMessaging` API, which all provides a `setAnalyticsLabel()` method. -- [changed] Upgraded the Cloud Firestore client to 1.9.0. -- [changed] Upgraded the Cloud Storage client to 1.79.0. -- [changed] Upgraded the Google API client to 1.30.1. - -# v6.8.1 - -- [fixed] Enabled automatic retries for FCM API calls failing with - HTTP 500 and 503 errors. - -# v6.8.0 - -- [added] Implemented new `sendAll()` and `sendMulticast()` APIs in - `FirebaseMessaging`. -- [changed] Removed org.json dependency and replaced with com.google.code.gson. -- [changed] Upgraded Mockito dependency, and fixed the build on Java 11. - -# v6.7.0 - -- [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 - -- [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. -- [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 - -# 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 - pairs in its payload. - -# v6.3.0 - -- [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. - -# v6.2.0 - -- [added] Added new `importUsersAsync()` API for bulk importing users - into Firebase Auth. - -# v6.1.0 - -- [changed] Deprecated the `FirebaseAuth.setCustomClaims()` method. - Developers should use the `FirebaseAuth.setCustomUserClaims()` method - instead. - -# v6.0.0 - -- [added] `FirebaseAuth`, `FirebaseMessaging` and `FirebaseInstanceId` - interfaces now expose a set of blocking methods. Each operation has - blocking an asynchronous versions. -- [changed] Removed the deprecated `FirebaseCredential` interface. -- [changed] Removed the deprecated `Task` interface along with the - `com.google.firebase.tasks` package. -- [changed] Dropped support for App Engine's Java 7 runtime. Developers - are advised to use the Admin SDK with Java 8 when deploying to App - Engine. -- [changed] Removed the deprecated `FirebaseDatabase.setLogLevel()` API - and the related logging utilities. Developers should use SLF4J to - configure logging directly. - -# v5.11.0 - -- [added] A new `FirebaseAuth.createSessionCookieAsync()` method for - creating a long-lived session cookie given a valid ID token. -- [added] A new `FirebaseAuth.verifySessionCookieAsync()` method for - verifying a given cookie string is valid. -- [fixed] Upgraded Cloud Firestore dependency version to 0.45.0-beta. -- [fixed] Upgraded Cloud Storage dependency version to 1.27.0. -- [fixed] Upgraded Netty dependency version to 4.1.22. - -# v5.10.0 - -- [fixed] Using the `HttpTransport` specified at `FirebaseOptions` in - `GooglePublicKeysManager`. This enables developers to use a custom - transport to fetch public keys when verifying ID tokens and session - cookies. -- [added] Connection timeout and read timeout for HTTP/REST connections - can now be configured via `FirebaseOptions.Builder` at app - initialization. -- [added] Added new `setMutableContent()`, `putCustomData()` and - `putAllCustomData()` methods to the `Aps.Builder` API. -- [fixed] Improved error handling in FCM by mapping more server-side - errors to client-side error codes. - -# v5.9.0 - -### Cloud Messaging - -- [feature] Added the `FirebaseCloudMessaging` API for sending - Firebase notifications and managing topic subscriptions. - -### Authentication - -- [added] The [`verifyIdTokenAsync()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#verifyIdTokenAsync) - method has an overload that accepts a boolean `checkRevoked` parameter. - When `true`, an additional check is performed to see whether the token - has been revoked. -- [added] A new [`revokeRefreshTokensAsync()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#revokeRefreshTokens) - method has been added to invalidate all tokens issued to a user. -- [added] A new getter `getTokensValidAfterTimestamp()` has been added - to the [`UserRecord`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/UserRecord) - class, which denotes the time before which tokens are not valid. - -### Realtime Database - -- [fixed] Exceptions thrown by database event handlers are now logged. - -### Initialization - -- [fixed] The [`FirebaseOptions.Builder.setStorageBucket()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseOptions.Builder.html#setStorageBucket(java.lang.String)) - method now throws a clear exception when invoked with a bucket URL - instead of the name. -- [fixed] Implemented a fix for a potential Guava version conflict which - was causing an `IllegalStateException` (precondition failure) in some - environments. - -### Cloud Firestore - -- [fixed] Upgraded the Cloud Firestore client to the latest available - version. - -# v5.8.0 - -### Initialization - -- [added] The [`FirebaseApp.initializeApp()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseApp.html#initializeApp()) - method now provides an overload that does not require any arguments. This - initializes an app using Google Application Default Credentials, and other - [`FirebaseOptions`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseOptions) - loaded from the `FIREBASE_CONFIG` environment variable. - -### Authentication - -- [changed] Improved error handling in user management APIs in the - [`FirebaseAuth`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth) - class. These operations now throw exceptions with well-defined - [error codes](https://firebase.google.com/docs/auth/admin/errors). - -### Realtime Database - -- [changed] The SDK now serializes large whole double values as longs when - appropriate. - -# v5.7.0 - -- [added] A new [`FirebaseInstanceId`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/iid/FirebaseInstanceId) - API that facilitates deleting instance IDs and associated user data from - Firebase projects. - -### Authentication -- [changed] No longer using `org.json` dependency in Authentication APIs, which - makes it easier to use the API in environments with conflicting JSON - libraries. - -# v5.6.0 - -- [changed] Upgraded the version of Google API Common dependency to the latest - (1.2.0). - -### Authentication -- [added] Added the - [`listUsersAsync()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#listUsersAsync(java.lang.String)) - method to the - [`FirebaseAuth`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth) - class. This method enables listing or iterating over all user accounts - in a Firebase project. -- [added] Added the - [`setCustomUserClaimsAsync()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#setCustomUserClaimsAsync(java.lang.String, java.util.Map)) - method to the - [`FirebaseAuth`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth) - class. This method enables setting custom claims on a Firebase user. - The custom claims can be accessed via that user's ID token. - -### Realtime Database -- [changed] Re-implemented the WebSocket communication layer of the - Realtime Database client using Netty. - -# v5.5.0 - -- [added] A new [`FirestoreClient`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/cloud/FirestoreClient) - API that enables access to [Cloud Firestore](https://firebase.google.com/docs/firestore) databases. - -### Realtime Database -- [changed] Ensured graceful termination of database worker threads upon - callng [`FirebaseApp.delete()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseApp.html#delete()). - -# v5.4.0 - -- [added] A new [`ThreadManager`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/ThreadManager) - API that can be used to specify the thread - pool and the `ThreadFactory` that should be used by the SDK. -- [added] All APIs that support asynchronous operations now return an - [`ApiFuture`](https://googleapis.github.io/api-common-java/1.1.0/apidocs/com/google/api/core/ApiFuture.html). - The old [`Task`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/tasks/Task) - API has been deprecated. For each method `x()` - that returns a `Task`, a new `xAsync()` method that returns an `ApiFuture` - has been introduced. -- [changed] The SDK now guarantees the graceful termination of all started - threads. In most environments, the SDK will use daemons for all background - activities. The developer can also initiate a graceful termination of threads - by calling [`FirebaseApp.delete()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseApp.html#delete()). - -### Initialization - -- [added] [`FirebaseOptions`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseOptions) - can now be initialized with - [`GoogleCredentials`](http://google.github.io/google-auth-library-java/releases/0.7.1/apidocs/com/google/auth/oauth2/GoogleCredentials.html). - This is the new recommended way to specify credentials when initializing the - SDK. The old [`FirebaseCredential`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseCredential) - and [`FirebaseCredentials`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseCredentials) - APIs have been deprecated. - -# v5.3.1 - -### Authentication -- [changed] Throwing an accurate and more detailed error from - [`verifyIdToken()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#verifyIdToken(java.lang.String)) - in the event of a low-level exception. - -### Realtime Database -- [changed] Proper handling and logging of exceptions thrown by the - [`onComplete()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/database/Transaction.Handler.html#onComplete(com.google.firebase.database.DatabaseError, boolean, com.google.firebase.database.DataSnapshot)) - event of transaction handlers. - -# v5.3.0 -- [added] A new [{{firebase_storage}} API](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/cloud/StorageClient) - that facilitates accessing Google Cloud Storage buckets using the - [`google-cloud-storage`](http://googlecloudplatform.github.io/google-cloud-java/latest/apidocs/com/google/cloud/storage/Storage.html) - library. -- [added] Integrated with the [SLF4J](https://www.slf4j.org/) library for all logging - purposes. - -### Authentication -- [added] Added the method - [`getUserByPhoneNumber()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#getUserByPhoneNumber(java.lang.String)) - to the [`FirebaseAuth`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth) - interface. This method - enables retrieving user profile information by a phone number. -- [added] [`CreateRequest`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/UserRecord.CreateRequest) - and [`UpdateRequest`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/UserRecord.UpdateRequest) types - now provide setters for specifying a phone number, which can be used to create users with a phone - number field and/or update the phone number associated with a user. -- [added] Added the `getPhoneNumber()` method to - [`UserRecord`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/UserRecord), - which exposes the phone number associated with a user account. -- [added] Added the `getPhoneNumber()` method to - [`UserInfo`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/UserInfo), - which exposes the phone number associated with a user account by a linked - identity provider. - -### Realtime Database -- {{changed}} Deprecated the `FirebaseDatabase.setLogLevel()` method. Use SLF4J to configure logging. -- [changed] Logging a detailed error when the database client fails to authenticate with the backend - Firebase servers. - -# v5.2.0 -- [added] New factory methods in the - [`FirebaseCredentials`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseCredentials) - class - that accept `HttpTransport` and `JsonFactory` arguments. These settings are - used when the credentials make HTTP calls to obtain OAuth2 access tokens. -- [added] New `setHttpTransport()` and `setJsonFactory()` methods in the - [`FirebaseOptions`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseOptions) - class. These settings are used by all services of the SDK except - `FirebaseDatabase`. - -# v5.1.0 - -### Authentication - -- [added] A new user management API that allows provisioning and managing - Firebase users from Java applications. This API adds `getUser()`, - `getUserByEmail()`, `createUser()`, `updateUser()` and `deleteUser()` methods - to the [`FirebaseAuth`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth) - interface. - -# v5.0.1 - -### Realtime Database - -- [changed] Fixed a database API thread leak that made the SDK - unstable when running in the Google App Engine environment. - -# v5.0.0 - -### Initialization - -- [added] Factory methods in - [`FirebaseCredentials`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseCredentials) - class can now throw `IOExceptions`, providing fail-fast semantics while - facilitating simpler error handling. -- [added] The deprecated `setServiceAccount()` method has been removed - from the - [`FirebaseOptions.Builder`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseOptions.Builder) - class in favor of the `setCredential()` method. -- [added] Trying to initialize the SDK without setting a credential now - results in an exception. -- {{changed}} The - [`FirebaseCredential`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseCredential) - interface now returns a new - [`GoogleOAuthAccessToken`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/GoogleOAuthAccessToken) - type, which encapsulates both token string and its expiry time. - -# v4.1.7 - -- [added] Introducing a new `FirebaseApp.delete()` method, which can - be used to gracefully shut down app instances. All app invocations after a - call to `delete()` will throw exceptions. Deleted app instances can also be - re-initialized with the same name if necessary. - -- [changed] Upgraded SDK dependencies. Guava, Google API Client, and JSON - libraries that the SDK depends on have been upgraded to more recent versions. - -# v4.1.6 - -### Realtime Database - -- [changed] Updated the SDK to select the correct thread pool - implementation when running on a regular JVM with App Engine - libraries in the `classpath`. - -# v4.1.5 - -### Realtime Database - -- [changed] Fixed the invalid SDK version constant in the `FirebaseDatabase` - class that was released in v4.1.4. - -# v4.1.4 - -### Authentication - -- [changed] Updated the SDK to periodically refresh the OAuth access token - internally used by `FirebaseApp`. This reduces the number of authentication - failures encountered at runtime by various SDK components (e.g. Realtime Database) - to nearly zero. This feature is active by default when running in typical - Java environments, or the Google App Engine environment with background - threads support. - -# v4.1.3 - -### Realtime Database - -- [changed] Updated Realtime Database to properly swap out the ID token used to - authenticate the underlying websocket when a new ID token is generated. The - websocket connection is still disconnected and reconnected every hour when an - ID token expires unless you manually call - [`getAccessToken`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseCredential.html#getAccessToken(boolean)) - on the `FirebaseCredential` used to authenticate the SDK. In a future - release, the SDK will proactively refresh ID tokens automatically before they - expire. - - -# v4.1.2 - -### Initialization - -- [changed] Updated `initalizeApp()` to synchronously read from an `InputStream` - to avoid issues with closing the stream after initializing the SDK. -- [changed] Improved confusing error messages when initializing the SDK with - a `null` or malformed `InputStream`. - - -# v4.1.1 -### Authentication - -- [changed] Fixed a dependency issue which caused the - [`verifyIdToken()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#verifyIdToken(java.lang.String)) - method to always throw an exception. - - -# v4.1.0 -### Initialization - -- {{deprecated}} The - [`FirebaseOptions.Builder.setServiceAccount()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseOptions.Builder.html#setServiceAccount(java.io.InputStream)) - method has been deprecated in favor of a new - [`FirebaseOptions.Builder.setCredential()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseOptions.Builder.html#setCredential(com.google.firebase.auth.FirebaseCredential)) - method. See [Initialize the SDK](https://firebase.google.com/docs/admin/setup/#initialize_the_sdk) for - usage instructions. -- [added] The new - [`FirebaseCredential.fromCertificate()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseCredentials.html#fromCertificate(java.io.InputStream)) - method allows you to authenticate the SDK with a service account certificate - file. -- [added] The new - [`FirebaseCredential.fromRefreshToken()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseCredentials.html#fromRefreshToken(java.io.InputStream)) - method allows you to authenticate the SDK with a Google OAuth2 refresh token. -- [added] The new - [`FirebaseCredential.applicationDefault()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseCredentials.html#applicationDefault()) - method allows you to authenticate the SDK with Google Application Default - Credentials. - -### Authentication - -- [issue] The - [`verifyIdToken()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#verifyIdToken(java.lang.String)) - method is broken in this release and throws an exception due to an incorrect - dependency. This was fixed in version [`4.1.1`](#4.1.1). - - -# v4.0.4 - -- [changed] Fixed issue which caused threads to be terminated in Google App - Engine after 24 hours, rendering the SDK unresponsive. -- [changed] Fixed issues which caused asynchronous task execution to fail on - automatically-scaled Google App Engine instances. - -### Authentication - -- [added] Improved error messages and added App Engine support for the - [`verifyIdToken()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#verifyIdToken(java.lang.String)) - method. - -### Realtime Database - -- [changed] Fixed a race condition which could occur when new writes are added - while the connection is being closed. - - -# v4.0.3 - -### Initialization - -- [changed] Fixed an issue that caused a `null` input to the - [`setDatabaseAuthVariableOverride()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseOptions.Builder.html#setDatabaseAuthVariableOverride(java.util.Map)) - method to be ignored, which caused the app to still have full admin access. - Now, passing this value has the expected behavior: the app has unauthenticated - access to the Realtime Database, and behaves as if no user is logged into the - app. - -### Realtime Database - -- [changed] Use of the - [`updateChildren()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/database/DatabaseReference.html#updateChildren(java.util.Map)) - method now only cancels transactions that are directly included in the updated - paths (not transactions in adjacent paths). For example, an update at `/move` - for a child node `walk` will cancel transactions at `/`, `/move`, and - `/move/walk` and in any child nodes under `/move/walk`. But, it will no longer - cancel transactions at sibling nodes, such as `/move/run`. - - -# v4.0.2 - -- [changed] This update restores Java 7 compatibilty for the Admin Java SDK. - - -# v4.0.1 - -- [changed] Fixed an issue with a missing dependency in the [`4.0.0`](#4.0.0) - JAR which caused the Database API to not work. -- [issue] This version was compiled for Java 8 and does not support Java 7. - This was fixed in version [`4.0.2`](#4.0.2). - - -# v4.0.0 - -- [added] The Admin Java SDK (available on Maven as `firebase-admin`) - replaces the pre-existing `firebase-server-sdk` Maven package, which is now - deprecated. See - [Add the Firebase Admin SDK to your Server](https://firebase.google.com/docs/admin/setup/) to get - started. -- [issue] This version is missing a dependency which causes the Database API - to not work. This was fixed in version [`4.0.1`](#4.0.1). -- [issue] This version was compiled for Java 8 and does not support Java 7. - This was fixed in version [`4.0.2`](#4.0.2). - -### Authentication - -- [changed] The - [`createCustomToken()`](https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/FirebaseAuth.html#createCustomToken(java.lang.String)) - method is now asynchronous, returning a `Task` instead of a `String`. - - diff --git a/prepare_release.sh b/prepare_release.sh index cc5969e0c..b531557ef 100755 --- a/prepare_release.sh +++ b/prepare_release.sh @@ -75,20 +75,6 @@ RELEASE_BRANCH="release-${TIMESTAMP}" echo "[INFO] Creating new release branch: ${RELEASE_BRANCH}" git checkout -b ${RELEASE_BRANCH} master -HOST=$(uname) -echo "[INFO] Updating CHANGELOG.md" -if [ $HOST == "Darwin" ]; then - sed -i "" -e "1 s/# Unreleased//" "CHANGELOG.md" -else - sed -i -e "/# Unreleased/d" "CHANGELOG.md" -fi - -echo -e "# Unreleased\n\n-\n\n# v${VERSION}" | cat - CHANGELOG.md > TEMP_CHANGELOG.md -mv TEMP_CHANGELOG.md CHANGELOG.md -git add CHANGELOG.md -git commit -m "Updating CHANGELOG for ${VERSION} release." -git push origin ${RELEASE_BRANCH} - ################################# # RUN MAVEN PREPARATION STEPS # From c33579dc569a2d0b3e857f9f2f3b75964be723ec Mon Sep 17 00:00:00 2001 From: chong-shao <31256040+chong-shao@users.noreply.github.com> Date: Wed, 23 Oct 2019 16:03:27 -0700 Subject: [PATCH 086/456] fix(fcm): Increase the multicast request count limit to 500. (#321) * Increase the multicast request count limit to 500. * Update integration test --- .../google/firebase/messaging/FirebaseMessaging.java | 12 ++++++------ .../google/firebase/messaging/MulticastMessage.java | 8 ++++---- .../firebase/messaging/FirebaseMessagingIT.java | 8 ++++---- .../firebase/messaging/FirebaseMessagingTest.java | 2 +- .../firebase/messaging/MulticastMessageTest.java | 4 ++-- .../firebase/snippets/FirebaseMessagingSnippets.java | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index 7330c27d3..ee25957b3 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -149,7 +149,7 @@ protected String execute() throws FirebaseMessagingException { *

      The responses list obtained by calling {@link BatchResponse#getResponses()} on the return * value corresponds to the order of input messages. * - * @param messages A non-null, non-empty list containing up to 100 messages. + * @param messages A non-null, non-empty list containing up to 500 messages. * @return A {@link BatchResponse} indicating the result of the operation. * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for * delivery. An exception here indicates a total failure -- i.e. none of the messages in the @@ -171,7 +171,7 @@ public BatchResponse sendAll( *

      The responses list obtained by calling {@link BatchResponse#getResponses()} on the return * value corresponds to the order of input messages. * - * @param messages A non-null, non-empty list containing up to 100 messages. + * @param messages A non-null, non-empty list containing up to 500 messages. * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. * @return A {@link BatchResponse} indicating the result of the operation. * @throws FirebaseMessagingException If an error occurs while handing the messages off to FCM for @@ -186,7 +186,7 @@ public BatchResponse sendAll( /** * Similar to {@link #sendAll(List)} but performs the operation asynchronously. * - * @param messages A non-null, non-empty list containing up to 100 messages. + * @param messages A non-null, non-empty list containing up to 500 messages. * @return @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when * the messages have been sent. */ @@ -197,7 +197,7 @@ public ApiFuture sendAllAsync(@NonNull List messages) { /** * Similar to {@link #sendAll(List, boolean)} but performs the operation asynchronously. * - * @param messages A non-null, non-empty list containing up to 100 messages. + * @param messages A non-null, non-empty list containing up to 500 messages. * @param dryRun A boolean indicating whether to perform a dry run (validation only) of the send. * @return @return An {@code ApiFuture} that will complete with a {@link BatchResponse} when * the messages have been sent, or when the emulation has finished. @@ -284,8 +284,8 @@ private CallableOperation sendAllOp( final List immutableMessages = ImmutableList.copyOf(messages); checkArgument(!immutableMessages.isEmpty(), "messages list must not be empty"); - checkArgument(immutableMessages.size() <= 100, - "messages list must not contain more than 100 elements"); + checkArgument(immutableMessages.size() <= 500, + "messages list must not contain more than 500 elements"); final FirebaseMessagingClient messagingClient = getMessagingClient(); return new CallableOperation() { @Override diff --git a/src/main/java/com/google/firebase/messaging/MulticastMessage.java b/src/main/java/com/google/firebase/messaging/MulticastMessage.java index 16434f6a9..96a6f19db 100644 --- a/src/main/java/com/google/firebase/messaging/MulticastMessage.java +++ b/src/main/java/com/google/firebase/messaging/MulticastMessage.java @@ -31,7 +31,7 @@ /** * Represents a message that can be sent to multiple devices via Firebase Cloud Messaging (FCM). * Contains payload information as well as the list of device registration tokens to which the - * message should be sent. A single {@code MulticastMessage} may contain up to 100 registration + * message should be sent. A single {@code MulticastMessage} may contain up to 500 registration * tokens. * *

      Instances of this class are thread-safe and immutable. Use {@link MulticastMessage.Builder} @@ -55,7 +55,7 @@ public class MulticastMessage { private MulticastMessage(Builder builder) { this.tokens = builder.tokens.build(); checkArgument(!this.tokens.isEmpty(), "at least one token must be specified"); - checkArgument(this.tokens.size() <= 100, "no more than 100 tokens can be specified"); + checkArgument(this.tokens.size() <= 500, "no more than 500 tokens can be specified"); for (String token : this.tokens) { checkArgument(!Strings.isNullOrEmpty(token), "none of the tokens can be null or empty"); } @@ -103,7 +103,7 @@ public static class Builder { private Builder() {} /** - * Adds a token to which the message should be sent. Up to 100 tokens can be specified on + * Adds a token to which the message should be sent. Up to 500 tokens can be specified on * a single instance of {@link MulticastMessage}. * * @param token A non-null, non-empty Firebase device registration token. @@ -115,7 +115,7 @@ public Builder addToken(@NonNull String token) { } /** - * Adds a collection of tokens to which the message should be sent. Up to 100 tokens can be + * Adds a collection of tokens to which the message should be sent. Up to 500 tokens can be * specified on a single instance of {@link MulticastMessage}. * * @param tokens Collection of Firebase device registration tokens. diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 8951d7cd8..4d9af55d0 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -114,16 +114,16 @@ public void testSendAll() throws Exception { } @Test - public void testSendHundred() throws Exception { + public void testSendFiveHundred() throws Exception { List messages = new ArrayList<>(); - for (int i = 0; i < 100; i++) { + for (int i = 0; i < 500; i++) { messages.add(Message.builder().setTopic("foo-bar-" + (i % 10)).build()); } BatchResponse response = FirebaseMessaging.getInstance().sendAll(messages, true); - assertEquals(100, response.getResponses().size()); - assertEquals(100, response.getSuccessCount()); + assertEquals(500, response.getResponses().size()); + assertEquals(500, response.getSuccessCount()); assertEquals(0, response.getFailureCount()); for (SendResponse sendResponse : response.getResponses()) { if (!sendResponse.isSuccessful()) { diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index d5f39ba46..bebba0864 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -296,7 +296,7 @@ public void testSendAllWithTooManyMessages() throws FirebaseMessagingException { MockFirebaseMessagingClient client = MockFirebaseMessagingClient.fromMessageId(null); FirebaseMessaging messaging = getMessagingForSend(Suppliers.ofInstance(client)); ImmutableList.Builder listBuilder = ImmutableList.builder(); - for (int i = 0; i < 101; i++) { + for (int i = 0; i < 501; i++) { listBuilder.add(Message.builder().setTopic("topic").build()); } diff --git a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java index 5cd053103..25018ed4c 100644 --- a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java @@ -70,12 +70,12 @@ public void testNoTokens() { @Test public void testTooManyTokens() { MulticastMessage.Builder builder = MulticastMessage.builder(); - for (int i = 0; i < 101; i++) { + for (int i = 0; i < 501; i++) { builder.addToken("token" + i); } try { builder.build(); - fail("No error thrown for more than 100 tokens"); + fail("No error thrown for more than 500 tokens"); } catch (IllegalArgumentException expected) { // expected } diff --git a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java index eb940215a..d6e4417e1 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java @@ -122,7 +122,7 @@ public void sendAll() throws FirebaseMessagingException { String registrationToken = "YOUR_REGISTRATION_TOKEN"; // [START send_all] - // Create a list containing up to 100 messages. + // Create a list containing up to 500 messages. List messages = Arrays.asList( Message.builder() .setNotification(new Notification("Price drop", "5% off all electronics")) From a8f5ba820c3b4015ffa9c1f9525081747f2ec369 Mon Sep 17 00:00:00 2001 From: chong-shao <31256040+chong-shao@users.noreply.github.com> Date: Mon, 28 Oct 2019 22:38:43 -0700 Subject: [PATCH 087/456] feat(fcm): Add 12 new android notification params support (#320) * Add 12 new parameter support for Android Notifications * Add LightSettings class * Add LightSettingsColor class --- .../messaging/AndroidNotification.java | 285 ++++++++++++++++++ .../firebase/messaging/LightSettings.java | 128 ++++++++ .../messaging/LightSettingsColor.java | 71 +++++ .../firebase/messaging/MessageTest.java | 84 ++++++ 4 files changed, 568 insertions(+) create mode 100644 src/main/java/com/google/firebase/messaging/LightSettings.java create mode 100644 src/main/java/com/google/firebase/messaging/LightSettingsColor.java diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index ef286d499..adb84c638 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -21,9 +21,14 @@ 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 java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; /** * Represents the Android-specific notification options that can be included in a {@link Message}. @@ -69,6 +74,51 @@ public class AndroidNotification { @Key("image") private final String image; + + @Key("ticker") + private final String ticker; + + @Key("sticky") + private final Boolean sticky; + + @Key("event_time") + private final String eventTime; + + @Key("local_only") + private final Boolean localOnly; + + @Key("notification_priority") + private final String priority; + + @Key("vibrate_timings") + private final List vibrateTimings; + + @Key("default_vibrate_timings") + private final Boolean defaultVibrateTimings; + + @Key("default_sound") + private final Boolean defaultSound; + + @Key("light_settings") + private final LightSettings lightSettings; + + @Key("default_light_settings") + private final Boolean defaultLightSettings; + + @Key("visibility") + private final String visibility; + + @Key("notification_count") + private final Integer notificationCount; + + private static final Map PRIORITY_MAP = + ImmutableMap.builder() + .put(Priority.MIN, "PRIORITY_MIN") + .put(Priority.LOW, "PRIORITY_LOW") + .put(Priority.DEFAULT, "PRIORITY_DEFAULT") + .put(Priority.HIGH, "PRIORITY_HIGH") + .put(Priority.MAX, "PRIORITY_MAX") + .build(); private AndroidNotification(Builder builder) { this.title = builder.title; @@ -101,6 +151,49 @@ private AndroidNotification(Builder builder) { } this.channelId = builder.channelId; this.image = builder.image; + this.ticker = builder.ticker; + this.sticky = builder.sticky; + this.eventTime = builder.eventTime; + this.localOnly = builder.localOnly; + if (builder.priority != null) { + this.priority = builder.priority.toString(); + } else { + this.priority = null; + } + if (!builder.vibrateTimings.isEmpty()) { + this.vibrateTimings = ImmutableList.copyOf(builder.vibrateTimings); + } else { + this.vibrateTimings = null; + } + this.defaultVibrateTimings = builder.defaultVibrateTimings; + this.defaultSound = builder.defaultSound; + this.lightSettings = builder.lightSettings; + this.defaultLightSettings = builder.defaultLightSettings; + if (builder.visibility != null) { + this.visibility = builder.visibility.name().toLowerCase(); + } else { + this.visibility = null; + } + this.notificationCount = builder.notificationCount; + } + + public enum Priority { + MIN, + LOW, + DEFAULT, + HIGH, + MAX; + + @Override + public String toString() { + return PRIORITY_MAP.get(this); + } + } + + public enum Visibility { + PRIVATE, + PUBLIC, + SECRET, } /** @@ -127,6 +220,18 @@ public static class Builder { private List titleLocArgs = new ArrayList<>(); private String channelId; private String image; + private String ticker; + private Boolean sticky; + private String eventTime; + private Boolean localOnly; + private Priority priority; + private List vibrateTimings = new ArrayList<>(); + private Boolean defaultVibrateTimings; + private Boolean defaultSound; + private LightSettings lightSettings; + private Boolean defaultLightSettings; + private Visibility visibility; + private Integer notificationCount; private Builder() {} @@ -309,6 +414,186 @@ public Builder setImage(String imageUrl) { return this; } + /** + * Sets the "ticker" text, which is sent to accessibility services. Prior to API level 21 + * (Lollipop), sets the text that is displayed in the status bar when the notification + * first arrives. + * + * @param ticker Ticker name. + * @return This builder. + */ + public Builder setTicker(String ticker) { + this.ticker = ticker; + return this; + } + + /** + * Sets the sticky flag. When set to false or unset, the notification is automatically + * dismissed when the user clicks it in the panel. When set to true, the notification + * persists even when the user clicks it. + * + * @param sticky The sticky flag + * @return This builder. + */ + public Builder setSticky(boolean sticky) { + this.sticky = sticky; + return this; + } + + /** + * For notifications that inform users about events with an absolute time reference, sets + * the time that the event in the notification occurred in milliseconds. Notifications + * in the panel are sorted by this time. The time is be formated in RFC3339 UTC "Zulu" + * format, accurate to nanoseconds. Example: "2014-10-02T15:01:23.045123456Z". Note that + * since the time is in milliseconds, the last section of the time representation always + * has 6 leading zeros. + * + * @param eventTimeInMillis The event time in milliseconds + * @return This builder. + */ + public Builder setEventTimeInMillis(long eventTimeInMillis) { + this.eventTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'") + .format(new Date(eventTimeInMillis)); + return this; + } + + /** + * Sets whether or not this notification is relevant only to the current device. Some + * notifications can be bridged to other devices for remote display, such as a Wear + * OS watch. This hint can be set to recommend this notification not be bridged. + * + * @param localOnly The "local only" flag + * @return This builder. + */ + public Builder setLocalOnly(boolean localOnly) { + this.localOnly = localOnly; + return this; + } + + /** + * Sets the relative priority for this notification. Priority is an indication of how much of + * the user's attention should be consumed by this notification. Low-priority notifications + * may be hidden from the user in certain situations, while the user might be interrupted + * for a higher-priority notification. + * + * @param priority The priority value, one of the values in {MIN, LOW, DEFAULT, HIGH, MAX} + * @return This builder. + */ + public Builder setPriority(Priority priority) { + this.priority = priority; + return this; + } + + /** + * Sets a list of vibration timings in milliseconds in the array to use. The first value in the + * array indicates the duration to wait before turning the vibrator on. The next value + * indicates the duration to keep the vibrator on. Subsequent values alternate between + * duration to turn the vibrator off and to turn the vibrator on. If {@code vibrate_timings} + * is set and {@code default_vibrate_timings} is set to true, the default value is used instead + * of the user-specified {@code vibrate_timings}. + * A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". + * + * @param vibrateTimingsInMillis List of vibration time in milliseconds + * @return This builder. + */ + public Builder setVibrateTimingsInMillis(long[] vibrateTimingsInMillis) { + List list = new ArrayList<>(); + for (long value : vibrateTimingsInMillis) { + checkArgument(value >= 0, "elements in vibrateTimingsInMillis must not be negative"); + long seconds = TimeUnit.MILLISECONDS.toSeconds(value); + long subsecondNanos = TimeUnit.MILLISECONDS.toNanos(value - seconds * 1000L); + if (subsecondNanos > 0) { + list.add(String.format("%d.%09ds", seconds, subsecondNanos)); + } else { + list.add(String.format("%ds", seconds)); + } + } + this.vibrateTimings = ImmutableList.copyOf(list); + return this; + } + + /** + * Sets the whether to use the default vibration timings. If set to true, use the Android + * framework's default vibrate pattern for the notification. Default values are specified + * in {@code config.xml}. If {@code default_vibrate_timings} is set to true and + * {@code vibrate_timings} is also set, the default value is used instead of the + * user-specified {@code vibrate_timings}. + * + * @param defaultVibrateTimings The flag indicating whether to use the default vibration timings + * @return This builder. + */ + public Builder setDefaultVibrateTimings(boolean defaultVibrateTimings) { + this.defaultVibrateTimings = defaultVibrateTimings; + return this; + } + + /** + * Sets the whether to use the default sound. If set to true, use the Android framework's + * default sound for the notification. Default values are specified in config.xml. + * + * @param defaultSound The flag indicating whether to use the default sound + * @return This builder. + */ + public Builder setDefaultSound(boolean defaultSound) { + this.defaultSound = defaultSound; + return this; + } + + /** + * Sets the settings to control the notification's LED blinking rate and color if LED is + * available on the device. The total blinking time is controlled by the OS. + * + * @param lightSettings The light settings to use + * @return This builder. + */ + public Builder setLightSettings(LightSettings lightSettings) { + this.lightSettings = lightSettings; + return this; + } + + /** + * Sets the whether to use the default light settings. If set to true, use the Android + * framework's default LED light settings for the notification. Default values are + * specified in config.xml. If {@code default_light_settings} is set to true and + * {@code light_settings} is also set, the user-specified {@code light_settings} is used + * instead of the default value. + * + * @param defaultLightSettings The flag indicating whether to use the default light + * settings + * @return This builder. + */ + public Builder setDefaultLightSettings(boolean defaultLightSettings) { + this.defaultLightSettings = defaultLightSettings; + return this; + } + + /** + * Sets the visibility of this notification. + * + * @param visibility The visibility value. one of the values in {PRIVATE, PUBLIC, SECRET} + * @return This builder. + */ + public Builder setVisibility(Visibility visibility) { + this.visibility = visibility; + return this; + } + + /** + * Sets the number of items this notification represents. May be displayed as a badge + * count for launchers that support badging. For example, this might be useful if you're + * using just one notification to represent multiple new messages but you want the count + * here to represent the number of total new messages. If zero or unspecified, systems + * that support badging use the default, which is to increment a number displayed on + * the long-press menu each time a new notification arrives. + * + * @param notificationCount The notification count + * @return This builder. + */ + public Builder setNotificationCount(int notificationCount) { + this.notificationCount = notificationCount; + return this; + } + /** * Creates a new {@link AndroidNotification} instance from the parameters set on this builder. * diff --git a/src/main/java/com/google/firebase/messaging/LightSettings.java b/src/main/java/com/google/firebase/messaging/LightSettings.java new file mode 100644 index 000000000..75a692090 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/LightSettings.java @@ -0,0 +1,128 @@ +/* + * Copyright 2019 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.api.client.util.Key; +import java.util.concurrent.TimeUnit; + +/** + * A class representing light settings in an Android Notification. + */ +public final class LightSettings { + + @Key("color") + private final LightSettingsColor color; + + @Key("light_on_duration") + private final String lightOnDuration; + + @Key("light_off_duration") + private final String lightOffDuration; + + private LightSettings(Builder builder) { + this.color = builder.color; + this.lightOnDuration = builder.lightOnDuration; + this.lightOffDuration = builder.lightOffDuration; + } + + /** + * Creates a new {@link LightSettings.Builder}. + * + * @return A {@link LightSettings.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private LightSettingsColor color; + private String lightOnDuration; + private String lightOffDuration; + + private Builder() {} + + /** + * Sets the lightSettingsColor value with a string. + * + * @param lightSettingsColor LightSettingsColor specified in the {@code #rrggbb} format. + * @return This builder. + */ + public Builder setColorFromString(String color) { + this.color = LightSettingsColor.fromString(color); + return this; + } + + /** + * Sets the lightSettingsColor value in the light settings. + * + * @param lightSettingsColor Color to be used in the light settings. + * @return This builder. + */ + public Builder setColor(LightSettingsColor color) { + this.color = color; + return this; + } + + /** + * Sets the light on duration in milliseconds. + * + * @param lightOnDurationInMillis The time duration in milliseconds for the LED light to be on. + * @return This builder. + */ + public Builder setLightOnDurationInMillis(long lightOnDurationInMillis) { + this.lightOnDuration = convertToSecondsAndNanosFormat(lightOnDurationInMillis); + return this; + } + + /** + * Sets the light off duration in milliseconds. + * + * @param lightOffDurationInMillis The time duration in milliseconds for the LED light to be + * off. + * @return This builder. + */ + public Builder setLightOffDurationInMillis(long lightOffDurationInMillis) { + this.lightOffDuration = convertToSecondsAndNanosFormat(lightOffDurationInMillis); + return this; + } + + private String convertToSecondsAndNanosFormat(long millis) { + checkArgument(millis >= 0, "Milliseconds duration must not be negative"); + long seconds = TimeUnit.MILLISECONDS.toSeconds(millis); + long subsecondNanos = TimeUnit.MILLISECONDS + .toNanos(millis - seconds * 1000L); + if (subsecondNanos > 0) { + return String.format("%d.%09ds", seconds, subsecondNanos); + } else { + return String.format("%ds", seconds); + } + } + + /** + * Builds a new {@link LightSettings} instance from the fields set on this builder. + * + * @return A non-null {@link LightSettings}. + * @throws IllegalArgumentException If the volume value is out of range. + */ + public LightSettings build() { + return new LightSettings(this); + } + } +} diff --git a/src/main/java/com/google/firebase/messaging/LightSettingsColor.java b/src/main/java/com/google/firebase/messaging/LightSettingsColor.java new file mode 100644 index 000000000..cfec64995 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/LightSettingsColor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019 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.api.client.util.Key; + +/** + * A class representing color in LightSettings. + */ +public final class LightSettingsColor { + + @Key("red") + private final Float red; + + @Key("green") + private final Float green; + + @Key("blue") + private final Float blue; + + @Key("alpha") + private final Float alpha; + + /** + * Creates a new {@link LightSettingsColor} using the given red, green, blue, and + * alpha values. + * + * @param red The red component. + * @param green The green component. + * @param blue The blue component. + * @param alpha The alpha component. + */ + public LightSettingsColor(float red, float green, float blue, float alpha) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + } + + /** + * Creates a new {@link LightSettingsColor} with a string. Alpha of the color will be + * set to 1. + * + * @param rrggbb LightSettingsColor specified in the {@code #rrggbb} format. + * @return A {@link LightSettingsColor} instance. + */ + public static LightSettingsColor fromString(String rrggbb) { + checkArgument(rrggbb.matches("^#[0-9a-fA-F]{6}$"), + "LightSettingsColor must be in the form #RRGGBB"); + float red = (float) Integer.parseInt(rrggbb.substring(1, 3), 16) / 255.0f; + float green = (float) Integer.valueOf(rrggbb.substring(3, 5), 16) / 255.0f; + float blue = (float) Integer.valueOf(rrggbb.substring(5, 7), 16) / 255.0f; + return new LightSettingsColor(red, green, blue, 1.0f); + } +} diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 556f2d9ea..7c8f528df 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -28,6 +28,9 @@ import com.google.firebase.messaging.AndroidConfig.Priority; import java.io.IOException; import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -796,6 +799,87 @@ public void testImageInApnsNotification() throws IOException { .build(); assertJsonEquals(expected, message); } + + @Test + public void testInvalidColorInAndroidNotificationLightSettings() throws IOException { + try { + LightSettings.Builder lightSettingsBuilder = LightSettings.builder() + .setColorFromString("#01020K") + .setLightOnDurationInMillis(1002L) + .setLightOffDurationInMillis(1003L); + + lightSettingsBuilder.build(); + fail("No error thrown for invalid notification"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testExtendedAndroidNotificationParameters() throws IOException { + long[] vibrateTimings = {1000L, 1001L}; + Message message = Message.builder() + .setNotification(new Notification("title", "body")) + .setAndroidConfig(AndroidConfig.builder() + .setNotification(AndroidNotification.builder() + .setTitle("android-title") + .setBody("android-body") + .setTicker("ticker") + .setSticky(true) + .setEventTimeInMillis(1546304523123L) + .setLocalOnly(true) + .setPriority(AndroidNotification.Priority.HIGH) + .setVibrateTimingsInMillis(vibrateTimings) + .setDefaultVibrateTimings(false) + .setDefaultSound(false) + .setLightSettings(LightSettings.builder() + .setColorFromString("#336699") + .setLightOnDurationInMillis(1002L) + .setLightOffDurationInMillis(1003L) + .build()) + .setDefaultLightSettings(false) + .setVisibility(AndroidNotification.Visibility.PUBLIC) + .setNotificationCount(10) + .build()) + .build()) + .setTopic("test-topic") + .build(); + Map notification = ImmutableMap.builder() + .put("title", "title") + .put("body", "body") + .build(); + String eventTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'") + .format(new Date(1546304523123L)); + Map androidConfig = ImmutableMap.builder() + .put("notification", ImmutableMap.builder() + .put("title", "android-title") + .put("body", "android-body") + .put("ticker", "ticker") + .put("sticky", true) + .put("event_time", eventTime) + .put("local_only", true) + .put("notification_priority", "PRIORITY_HIGH") + .put("vibrate_timings", ImmutableList.of("1s", "1.001000000s")) + .put("default_vibrate_timings", false) + .put("default_sound", false) + .put("light_settings", ImmutableMap.builder() + .put("color", ImmutableMap.builder() + .put("red", new BigDecimal(new BigInteger("2"), 1)) + .put("green", new BigDecimal(new BigInteger("4"), 1)) + .put("blue", new BigDecimal(new BigInteger("6"), 1)) + .put("alpha", new BigDecimal(new BigInteger("10"), 1)) + .build()) + .put("light_on_duration", "1.002000000s") + .put("light_off_duration", "1.003000000s") + .build()) + .put("default_light_settings", false) + .put("visibility", "public") + .put("notification_count", new BigDecimal(10)) + .build()) + .build(); + assertJsonEquals(ImmutableMap.of( + "topic", "test-topic", "notification", notification, "android", androidConfig), message); + } private static void assertJsonEquals( Map expected, Object actual) throws IOException { From 65ba409e4566b24d31ed5e04c5d1524819e3b38b Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 30 Oct 2019 15:35:16 -0400 Subject: [PATCH 088/456] Fix a typo in AndroidNotification (#324) * Fix a common typo in AndroidNotification * Update text --- .../java/com/google/firebase/messaging/AndroidNotification.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index adb84c638..121601407 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -443,7 +443,7 @@ public Builder setSticky(boolean sticky) { /** * For notifications that inform users about events with an absolute time reference, sets * the time that the event in the notification occurred in milliseconds. Notifications - * in the panel are sorted by this time. The time is be formated in RFC3339 UTC "Zulu" + * in the panel are sorted by this time. The time is formatted in RFC3339 UTC "Zulu" * format, accurate to nanoseconds. Example: "2014-10-02T15:01:23.045123456Z". Note that * since the time is in milliseconds, the last section of the time representation always * has 6 leading zeros. From bb8e34f12665f432d284785f69b33e61085763c7 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 30 Oct 2019 19:36:09 -0400 Subject: [PATCH 089/456] Admin Java SDK Release 6.11.0 (#325) * [maven-release-plugin] prepare release v6.11.0 * [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cffff27c4..c2b896fb9 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.10.1-SNAPSHOT + 6.11.1-SNAPSHOT jar firebase-admin From b7b712527cb97458cd6d0356f1c30837301197c8 Mon Sep 17 00:00:00 2001 From: rsgowman Date: Tue, 5 Nov 2019 10:42:41 -0500 Subject: [PATCH 090/456] Reject rounds=0 for SHA1 hashes (#326) Port of https://github.com/firebase/firebase-admin-node/pull/677 --- .../com/google/firebase/auth/hash/Md5.java | 2 +- .../com/google/firebase/auth/hash/Sha1.java | 2 +- .../com/google/firebase/auth/hash/Sha256.java | 2 +- .../com/google/firebase/auth/hash/Sha512.java | 2 +- .../firebase/auth/hash/InvalidHashTest.java | 34 +++++++++++++++---- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/hash/Md5.java b/src/main/java/com/google/firebase/auth/hash/Md5.java index f45362ae3..2abbe55ba 100644 --- a/src/main/java/com/google/firebase/auth/hash/Md5.java +++ b/src/main/java/com/google/firebase/auth/hash/Md5.java @@ -23,7 +23,7 @@ public class Md5 extends RepeatableHash { private Md5(Builder builder) { - super("MD5", 0, 120000, builder); + super("MD5", 0, 8192, builder); } public static Builder builder() { diff --git a/src/main/java/com/google/firebase/auth/hash/Sha1.java b/src/main/java/com/google/firebase/auth/hash/Sha1.java index d14975fda..385f4310c 100644 --- a/src/main/java/com/google/firebase/auth/hash/Sha1.java +++ b/src/main/java/com/google/firebase/auth/hash/Sha1.java @@ -23,7 +23,7 @@ public class Sha1 extends RepeatableHash { private Sha1(Builder builder) { - super("SHA1", 0, 120000, builder); + super("SHA1", 1, 8192, builder); } public static Builder builder() { diff --git a/src/main/java/com/google/firebase/auth/hash/Sha256.java b/src/main/java/com/google/firebase/auth/hash/Sha256.java index ecc0e7280..f65aee19a 100644 --- a/src/main/java/com/google/firebase/auth/hash/Sha256.java +++ b/src/main/java/com/google/firebase/auth/hash/Sha256.java @@ -23,7 +23,7 @@ public class Sha256 extends RepeatableHash { private Sha256(Builder builder) { - super("SHA256", 0, 120000, builder); + super("SHA256", 1, 8192, builder); } public static Builder builder() { diff --git a/src/main/java/com/google/firebase/auth/hash/Sha512.java b/src/main/java/com/google/firebase/auth/hash/Sha512.java index 858d16e05..e582520a9 100644 --- a/src/main/java/com/google/firebase/auth/hash/Sha512.java +++ b/src/main/java/com/google/firebase/auth/hash/Sha512.java @@ -23,7 +23,7 @@ public class Sha512 extends RepeatableHash { private Sha512(Builder builder) { - super("SHA512", 0, 120000, builder); + super("SHA512", 1, 8192, builder); } public static Builder builder() { diff --git a/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java b/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java index aabe4444c..5186a443a 100644 --- a/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java +++ b/src/test/java/com/google/firebase/auth/hash/InvalidHashTest.java @@ -48,17 +48,21 @@ public void testInvalidHmac() { @Test public void testInvalidRepeatableHash() { + // TODO(rsgowman): Once we can update to Java8, we could just do something like this instead of + // having all of the helpers: + // assertThrows(IllegalArgumentException.class, ()-> Md5.builder().setRounds(-1).build()); + List builders = ImmutableList.builder() - .add(Sha512.builder().setRounds(-1)) - .add(Sha256.builder().setRounds(-1)) - .add(Sha1.builder().setRounds(-1)) + .add(Sha512.builder().setRounds(0)) + .add(Sha256.builder().setRounds(0)) + .add(Sha1.builder().setRounds(0)) .add(Md5.builder().setRounds(-1)) .add(Pbkdf2Sha256.builder().setRounds(-1)) .add(PbkdfSha1.builder().setRounds(-1)) - .add(Sha512.builder().setRounds(120001)) - .add(Sha256.builder().setRounds(120001)) - .add(Sha1.builder().setRounds(120001)) - .add(Md5.builder().setRounds(120001)) + .add(Sha512.builder().setRounds(8193)) + .add(Sha256.builder().setRounds(8193)) + .add(Sha1.builder().setRounds(8193)) + .add(Md5.builder().setRounds(8193)) .add(Pbkdf2Sha256.builder().setRounds(120001)) .add(PbkdfSha1.builder().setRounds(120001)) .build(); @@ -72,6 +76,22 @@ public void testInvalidRepeatableHash() { } } + @Test + public void testValidRepeatableHash() { + Md5.builder().setRounds(0).build(); + Md5.builder().setRounds(8192).build(); + Sha1.builder().setRounds(1).build(); + Sha1.builder().setRounds(8192).build(); + Sha256.builder().setRounds(1).build(); + Sha256.builder().setRounds(8192).build(); + Sha512.builder().setRounds(1).build(); + Sha512.builder().setRounds(8192).build(); + PbkdfSha1.builder().setRounds(0).build(); + PbkdfSha1.builder().setRounds(120000).build(); + Pbkdf2Sha256.builder().setRounds(0).build(); + Pbkdf2Sha256.builder().setRounds(120000).build(); + } + @Test public void testInvalidScrypt() { List builders = ImmutableList.of( From 76f0f957a76fbf6ed303a21122430d3ba9f9fc00 Mon Sep 17 00:00:00 2001 From: sakebook Date: Thu, 7 Nov 2019 03:50:32 +0900 Subject: [PATCH 091/456] Fix Date test in another Locale (#327) * Add Locale.ROOT use SimpleDateFormat * Fix SimpleDateFormat locale use UK * Change default locale to US --- .../java/com/google/firebase/internal/DateUtils.java | 7 ++++--- .../internal/RetryUnsuccessfulResponseHandlerTest.java | 5 ++++- .../java/com/google/firebase/messaging/MessageTest.java | 9 +++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/google/firebase/internal/DateUtils.java b/src/main/java/com/google/firebase/internal/DateUtils.java index 6c4eeb7b0..a55fac8c0 100644 --- a/src/main/java/com/google/firebase/internal/DateUtils.java +++ b/src/main/java/com/google/firebase/internal/DateUtils.java @@ -20,6 +20,7 @@ import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; +import java.util.Locale; import java.util.TimeZone; /** @@ -57,9 +58,9 @@ final class DateUtils { static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy"; private static final SimpleDateFormat[] DEFAULT_PATTERNS = new SimpleDateFormat[] { - new SimpleDateFormat(PATTERN_RFC1123), - new SimpleDateFormat(PATTERN_RFC1036), - new SimpleDateFormat(PATTERN_ASCTIME) + new SimpleDateFormat(PATTERN_RFC1123, Locale.US), + new SimpleDateFormat(PATTERN_RFC1036, Locale.US), + new SimpleDateFormat(PATTERN_ASCTIME, Locale.US) }; static final TimeZone GMT = TimeZone.getTimeZone("GMT"); diff --git a/src/test/java/com/google/firebase/internal/RetryUnsuccessfulResponseHandlerTest.java b/src/test/java/com/google/firebase/internal/RetryUnsuccessfulResponseHandlerTest.java index 077e9e52f..f0bc2d0b3 100644 --- a/src/test/java/com/google/firebase/internal/RetryUnsuccessfulResponseHandlerTest.java +++ b/src/test/java/com/google/firebase/internal/RetryUnsuccessfulResponseHandlerTest.java @@ -35,7 +35,9 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.TimeZone; + import org.junit.Test; public class RetryUnsuccessfulResponseHandlerTest { @@ -137,7 +139,8 @@ public void testRetryAfterGivenAsSeconds() throws IOException { @Test public void testRetryAfterGivenAsDate() throws IOException { - SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz"); + SimpleDateFormat dateFormat = + new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); Date date = new Date(1000); Clock clock = new FixedClock(date.getTime()); diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 7c8f528df..562466f4a 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -33,6 +33,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; import org.junit.Test; @@ -769,7 +770,7 @@ public void testImageInAndroidNotification() throws IOException { assertJsonEquals(ImmutableMap.of( "topic", "test-topic", "notification", notification, "android", androidConfig), message); } - + @Test public void testImageInApnsNotification() throws IOException { Message message = Message.builder() @@ -799,7 +800,7 @@ public void testImageInApnsNotification() throws IOException { .build(); assertJsonEquals(expected, message); } - + @Test public void testInvalidColorInAndroidNotificationLightSettings() throws IOException { try { @@ -814,7 +815,7 @@ public void testInvalidColorInAndroidNotificationLightSettings() throws IOExcept // expected } } - + @Test public void testExtendedAndroidNotificationParameters() throws IOException { long[] vibrateTimings = {1000L, 1001L}; @@ -848,7 +849,7 @@ public void testExtendedAndroidNotificationParameters() throws IOException { .put("title", "title") .put("body", "body") .build(); - String eventTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'") + String eventTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'", Locale.US) .format(new Date(1546304523123L)); Map androidConfig = ImmutableMap.builder() .put("notification", ImmutableMap.builder() From e918da8b4f8e9112c3e981a28f24653849c0b678 Mon Sep 17 00:00:00 2001 From: Adrian Regan Date: Tue, 12 Nov 2019 05:30:09 +0000 Subject: [PATCH 092/456] feat(fcm): Updated code for setting Notification Count parameter in `AndroidNotification` class (#309) * Enforce positive integer for the notification count parameter * Update code comments --- .../messaging/AndroidNotification.java | 22 ++++++++++++------- .../firebase/messaging/MessageTest.java | 10 +++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index 121601407..08a872c50 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -119,7 +119,7 @@ public class AndroidNotification { .put(Priority.HIGH, "PRIORITY_HIGH") .put(Priority.MAX, "PRIORITY_MAX") .build(); - + private AndroidNotification(Builder builder) { this.title = builder.title; this.body = builder.body; @@ -150,7 +150,7 @@ private AndroidNotification(Builder builder) { this.titleLocArgs = null; } this.channelId = builder.channelId; - this.image = builder.image; + this.image = builder.image; this.ticker = builder.ticker; this.sticky = builder.sticky; this.eventTime = builder.eventTime; @@ -174,6 +174,10 @@ private AndroidNotification(Builder builder) { } else { this.visibility = null; } + if (builder.notificationCount != null) { + checkArgument(builder.notificationCount >= 0, + "notificationCount if specified must be zero or positive valued"); + } this.notificationCount = builder.notificationCount; } @@ -220,6 +224,7 @@ public static class Builder { private List titleLocArgs = new ArrayList<>(); private String channelId; private String image; + private Integer notificationCount; private String ticker; private Boolean sticky; private String eventTime; @@ -231,7 +236,6 @@ public static class Builder { private LightSettings lightSettings; private Boolean defaultLightSettings; private Visibility visibility; - private Integer notificationCount; private Builder() {} @@ -580,13 +584,15 @@ public Builder setVisibility(Visibility visibility) { /** * Sets the number of items this notification represents. May be displayed as a badge - * count for launchers that support badging. For example, this might be useful if you're - * using just one notification to represent multiple new messages but you want the count - * here to represent the number of total new messages. If zero or unspecified, systems - * that support badging use the default, which is to increment a number displayed on + * count for launchers that support badging. + * If not invoked then notification count is left unchanged. + * For example, this might be useful if you're using just one notification to represent + * multiple new messages but you want the count here to represent the number of total + * new messages. If zero or unspecified, systems that support badging use the default, + * which is to increment a number displayed on * the long-press menu each time a new notification arrives. * - * @param notificationCount The notification count + * @param notificationCount Zero or positive value. Zero indicates leave unchanged. * @return This builder. */ public Builder setNotificationCount(int notificationCount) { diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 562466f4a..5c8c3cbbf 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -167,6 +167,7 @@ public void testAndroidMessageWithNotification() throws IOException { .addBodyLocalizationArg("body-arg1") .addAllBodyLocalizationArgs(ImmutableList.of("body-arg2", "body-arg3")) .setChannelId("channel-id") + .setNotificationCount(4) .build()) .build()) .setTopic("test-topic") @@ -184,6 +185,10 @@ public void testAndroidMessageWithNotification() throws IOException { .put("body_loc_key", "body-loc") .put("body_loc_args", ImmutableList.of("body-arg1", "body-arg2", "body-arg3")) .put("channel_id", "channel-id") + // There is a problem with the JsonParser assignment to BigDecimal takes priority over + // all other number types and so this integer value is interpreted as a BigDecimal + // rather than an Integer. + .put("notification_count", BigDecimal.valueOf(4L)) .build(); Map data = ImmutableMap.of( "collapse_key", "test-key", @@ -195,6 +200,11 @@ public void testAndroidMessageWithNotification() throws IOException { assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); } + @Test(expected = IllegalArgumentException.class) + public void testAndroidNotificationWithNegativeCount() throws IllegalArgumentException { + AndroidNotification.builder().setNotificationCount(-1).build(); + } + @Test public void testAndroidMessageWithoutLocalization() throws IOException { Message message = Message.builder() From 646c3849cb4095bf08a42f6cf62079d52378b968 Mon Sep 17 00:00:00 2001 From: Anthony Chuinard Date: Thu, 5 Dec 2019 12:00:39 -0600 Subject: [PATCH 093/456] Upgrade Cloud Firestore dependency (#331) Will provide IN query features --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c2b896fb9..3a89e361d 100644 --- a/pom.xml +++ b/pom.xml @@ -422,7 +422,7 @@ com.google.cloud google-cloud-firestore - 1.21.0 + 1.31.0 From 5f1e48c341f1ad39b6d6db11a9e7d6dec7f0bb71 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 10 Dec 2019 13:37:21 -0800 Subject: [PATCH 094/456] Correctly handling FCM THIRD_PARTY_AUTH_ERROR error code (#333) --- .../messaging/FirebaseMessagingClientImpl.java | 5 +++-- .../FirebaseMessagingClientImplTest.java | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java index 92dfd0c53..c2659a270 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java @@ -67,14 +67,15 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient { .put("NOT_FOUND", "registration-token-not-registered") .put("PERMISSION_DENIED", "mismatched-credential") .put("RESOURCE_EXHAUSTED", "message-rate-exceeded") - .put("UNAUTHENTICATED", "invalid-apns-credentials") + .put("UNAUTHENTICATED", "third-party-auth-error") // FCM v1 new error codes - .put("APNS_AUTH_ERROR", "invalid-apns-credentials") + .put("APNS_AUTH_ERROR", "third-party-auth-error") .put("INTERNAL", FirebaseMessaging.INTERNAL_ERROR) .put("INVALID_ARGUMENT", "invalid-argument") .put("QUOTA_EXCEEDED", "message-rate-exceeded") .put("SENDER_ID_MISMATCH", "mismatched-credential") + .put("THIRD_PARTY_AUTH_ERROR", "third-party-auth-error") .put("UNAVAILABLE", "server-unavailable") .put("UNREGISTERED", "registration-token-not-registered") .build(); diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java index ae1136bf6..0b84e008e 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -249,6 +249,24 @@ public void testSendErrorWithFcmError() { } } + @Test + public void testSendErrorWithThirdPartyError() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " + + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + + ".v1.FcmError\", \"errorCode\": \"THIRD_PARTY_AUTH_ERROR\"}]}}"); + + try { + client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, "third-party-auth-error"); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + @Test public void testSendAll() throws Exception { final TestResponseInterceptor interceptor = new TestResponseInterceptor(); From 0d9b27ed892ba83cff733bf4400c7f7ad9043c08 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 12 Dec 2019 15:10:47 -0800 Subject: [PATCH 095/456] Fixing some failing RTDB tests (#334) * Fixing some failing RTDB tests * Using explicit options * Refactored options initialization --- .../firebase/database/FirebaseDatabaseTest.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java index 74e835884..d8395035e 100644 --- a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java +++ b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java @@ -38,11 +38,15 @@ public class FirebaseDatabaseTest { - private static FirebaseOptions firebaseOptions = + private static final FirebaseOptions firebaseOptions = new FirebaseOptions.Builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://firebase-db-test.firebaseio.com") .build(); + private static final FirebaseOptions firebaseOptionsWithoutDatabaseUrl = + new FirebaseOptions.Builder() + .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) + .build(); @Test public void testGetInstance() { @@ -210,9 +214,10 @@ public void testDbUrlIsEmulatorUrlWhenSettingOptionsManually() { new CustomTestCase("https://test.firebaseio.com?ns=valid-namespace", "localhost:90", "http://localhost:90", "valid-namespace") ); + for (CustomTestCase tc : testCases) { try { - FirebaseApp app = FirebaseApp.initializeApp(); + FirebaseApp app = FirebaseApp.initializeApp(firebaseOptionsWithoutDatabaseUrl); TestUtils.setEnvironmentVariables( ImmutableMap.of(EmulatorHelper.FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR, Strings.nullToEmpty(tc.envVariableUrl))); @@ -249,7 +254,7 @@ public void testDbUrlIsEmulatorUrlForDbRefWithPath() { for (CustomTestCase tc : testCases) { try { - FirebaseApp app = FirebaseApp.initializeApp(); + FirebaseApp app = FirebaseApp.initializeApp(firebaseOptionsWithoutDatabaseUrl); TestUtils.setEnvironmentVariables( ImmutableMap.of(EmulatorHelper.FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR, Strings.nullToEmpty(tc.envVariableUrl))); From 83b7a64c103fbf20f46c04df229000529e9a8d2d Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 18 Dec 2019 15:15:10 -0500 Subject: [PATCH 096/456] Staged Release 6.12.0 (#336) * [maven-release-plugin] prepare release v6.12.0 * [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3a89e361d..aa8d3c8b2 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.11.1-SNAPSHOT + 6.12.1-SNAPSHOT jar firebase-admin From 78852a97cc3543aa0a561a4469f89d1a49ef45cd Mon Sep 17 00:00:00 2001 From: chong-shao <31256040+chong-shao@users.noreply.github.com> Date: Thu, 19 Dec 2019 14:07:03 -0800 Subject: [PATCH 097/456] fix(fcm): Fix issues in event_time timestamp parsing (#338) * fix issues in event_time timestamp conversion * fix the formatting issue in event_time timestamp --- .../com/google/firebase/messaging/AndroidNotification.java | 6 ++++-- .../java/com/google/firebase/messaging/MessageTest.java | 4 +--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidNotification.java b/src/main/java/com/google/firebase/messaging/AndroidNotification.java index 08a872c50..05e680910 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidNotification.java +++ b/src/main/java/com/google/firebase/messaging/AndroidNotification.java @@ -28,6 +28,7 @@ import java.util.Date; import java.util.List; import java.util.Map; +import java.util.TimeZone; import java.util.concurrent.TimeUnit; /** @@ -456,8 +457,9 @@ public Builder setSticky(boolean sticky) { * @return This builder. */ public Builder setEventTimeInMillis(long eventTimeInMillis) { - this.eventTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'") - .format(new Date(eventTimeInMillis)); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS000000'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + this.eventTime = dateFormat.format(new Date(eventTimeInMillis)); return this; } diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 5c8c3cbbf..778b4f109 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -859,15 +859,13 @@ public void testExtendedAndroidNotificationParameters() throws IOException { .put("title", "title") .put("body", "body") .build(); - String eventTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'", Locale.US) - .format(new Date(1546304523123L)); Map androidConfig = ImmutableMap.builder() .put("notification", ImmutableMap.builder() .put("title", "android-title") .put("body", "android-body") .put("ticker", "ticker") .put("sticky", true) - .put("event_time", eventTime) + .put("event_time", "2019-01-01T01:02:03.123000000Z") .put("local_only", true) .put("notification_priority", "PRIORITY_HIGH") .put("vibrate_timings", ImmutableList.of("1s", "1.001000000s")) From 592453fc3b75bfbb13ba252e00711d9eaef95fe5 Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Thu, 9 Jan 2020 11:21:10 -0500 Subject: [PATCH 098/456] Update project.json to include the .NET SDK. (#339) The .NET SDK was missing from project.json, so this change simply adds it. --- .opensource/project.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.opensource/project.json b/.opensource/project.json index 74e68f048..56484ef37 100644 --- a/.opensource/project.json +++ b/.opensource/project.json @@ -7,6 +7,7 @@ "content": "README.md", "pages": [], "related": [ + "firebase/firebase-admin-dotnet", "firebase/firebase-admin-go", "firebase/firebase-admin-node", "firebase/firebase-admin-python" From 5255c4f0b3e770201940310d9fba1d367abdf85b Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 13 Jan 2020 12:44:00 -0800 Subject: [PATCH 099/456] Setting up a CI pipeline based on GitHub Actions (#342) * Setting up a CI pipeline based on GitHub Actions * Renamed file --- .github/workflows/ci.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..8b92d7bf0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +name: Java Continuous Integration + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up JDK 1.7 + uses: actions/setup-java@v1 + with: + java-version: 1.7 + - name: Build with Maven + run: mvn -B package --file pom.xml From 5ef443df5219d0aaa9a49818ba7192bcd385da80 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 14 Jan 2020 11:57:49 -0800 Subject: [PATCH 100/456] Removing Travis and GCB integrations (#347) * Removing Travis and GCB integrations * Updated CI badge --- .github/workflows/ci.yml | 4 +--- .travis.yml | 1 - README.md | 2 +- cloudbuild.yaml | 4 ---- 4 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 .travis.yml delete mode 100644 cloudbuild.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b92d7bf0..10007c5c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,10 @@ -name: Java Continuous Integration +name: Continuous Integration on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest - steps: - uses: actions/checkout@v1 - name: Set up JDK 1.7 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index dff5f3a5d..000000000 --- a/.travis.yml +++ /dev/null @@ -1 +0,0 @@ -language: java diff --git a/README.md b/README.md index 65a80bed0..406223d06 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/firebase/firebase-admin-java.svg?branch=master)](https://travis-ci.org/firebase/firebase-admin-java) +[![Build Status](https://github.com/firebase/firebase-admin-java/workflows/Continuous%20Integration/badge.svg)](https://github.com/firebase/firebase-admin-java/actions) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.google.firebase/firebase-admin/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.google.firebase/firebase-admin) [![Javadoc](https://javadoc-badge.appspot.com/com.google.firebase/firebase-admin.svg)](https://firebase.google.com/docs/reference/admin/java/reference/packages) diff --git a/cloudbuild.yaml b/cloudbuild.yaml deleted file mode 100644 index 77e86375e..000000000 --- a/cloudbuild.yaml +++ /dev/null @@ -1,4 +0,0 @@ -steps: - - name: 'maven:3-jdk-7' - entrypoint: 'mvn' - args: ['test', '-V', '-B'] From 551b7b79390b5b6f76f43003c4869aae687b1d3d Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 23 Jan 2020 14:15:22 -0500 Subject: [PATCH 101/456] Staged Release 6.12.1 (#351) * [maven-release-plugin] prepare release v6.12.1 * [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index aa8d3c8b2..e82439e48 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.12.1-SNAPSHOT + 6.12.2-SNAPSHOT jar firebase-admin From 6457976d0602f73ec2acef4806fbbe44c1785ae3 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 29 Jan 2020 10:43:19 -0800 Subject: [PATCH 102/456] Enabled automatic retries for FirebaseUserManager (#355) --- .../google/firebase/auth/FirebaseAuth.java | 21 ++++-- .../firebase/auth/FirebaseUserManager.java | 15 ++-- .../firebase/auth/FirebaseAuthTest.java | 8 +-- .../auth/FirebaseUserManagerTest.java | 71 +++++++++---------- 4 files changed, 63 insertions(+), 52 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 4c488cfea..f7f6231ad 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -75,12 +75,7 @@ private FirebaseAuth(Builder builder) { this.tokenFactory = threadSafeMemoize(builder.tokenFactory); this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier); this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier); - this.userManager = threadSafeMemoize(new Supplier() { - @Override - public FirebaseUserManager get() { - return new FirebaseUserManager(firebaseApp); - } - }); + this.userManager = threadSafeMemoize(builder.userManager); this.jsonFactory = firebaseApp.getOptions().getJsonFactory(); } @@ -1109,10 +1104,10 @@ protected String execute() throws FirebaseAuthException { } private Supplier threadSafeMemoize(final Supplier supplier) { - checkNotNull(supplier); return Suppliers.memoize(new Supplier() { @Override public T get() { + checkNotNull(supplier); synchronized (lock) { checkNotDestroyed(); return supplier.get(); @@ -1155,6 +1150,12 @@ public FirebaseTokenVerifier get() { return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); } }) + .setUserManager(new Supplier() { + @Override + public FirebaseUserManager get() { + return new FirebaseUserManager(app); + } + }) .build(); } @@ -1168,6 +1169,7 @@ static class Builder { private Supplier tokenFactory; private Supplier idTokenVerifier; private Supplier cookieVerifier; + private Supplier userManager; private Builder() { } @@ -1191,6 +1193,11 @@ Builder setCookieVerifier(Supplier cookieVerifi return this; } + Builder setUserManager(Supplier userManager) { + this.userManager = userManager; + return this; + } + FirebaseAuth build() { return new FirebaseAuth(this); } diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 8298499b4..03c2813bc 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -26,7 +26,6 @@ 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.HttpTransport; import com.google.api.client.http.json.JsonHttpContent; import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; @@ -45,7 +44,7 @@ import com.google.firebase.auth.internal.HttpErrorResponse; import com.google.firebase.auth.internal.UploadAccountResponse; -import com.google.firebase.internal.FirebaseRequestInitializer; +import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; import com.google.firebase.internal.SdkUtils; @@ -111,6 +110,10 @@ class FirebaseUserManager { * @param app A non-null {@link FirebaseApp}. */ FirebaseUserManager(@NonNull FirebaseApp app) { + this(app, null); + } + + FirebaseUserManager(@NonNull FirebaseApp app, @Nullable HttpRequestFactory requestFactory) { checkNotNull(app, "FirebaseApp must not be null"); String projectId = ImplFirebaseTrampolines.getProjectId(app); checkArgument(!Strings.isNullOrEmpty(projectId), @@ -119,8 +122,12 @@ class FirebaseUserManager { + "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)); + + if (requestFactory == null) { + requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + } + + this.requestFactory = requestFactory; } @VisibleForTesting diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index cf3b68385..1bc05174f 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -429,11 +429,11 @@ private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVe private FirebaseAuth getAuthForIdTokenVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); + FirebaseUserManager userManager = new FirebaseUserManager(app); return FirebaseAuth.builder() .setFirebaseApp(app) - .setTokenFactory(Suppliers.ofInstance(null)) .setIdTokenVerifier(tokenVerifierSupplier) - .setCookieVerifier(Suppliers.ofInstance(null)) + .setUserManager(Suppliers.ofInstance(userManager)) .build(); } @@ -444,11 +444,11 @@ private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier t private FirebaseAuth getAuthForSessionCookieVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); + FirebaseUserManager userManager = new FirebaseUserManager(app); return FirebaseAuth.builder() .setFirebaseApp(app) - .setTokenFactory(Suppliers.ofInstance(null)) - .setIdTokenVerifier(Suppliers.ofInstance(null)) .setCookieVerifier(tokenVerifierSupplier) + .setUserManager(Suppliers.ofInstance(userManager)) .build(); } diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 97ff7447a..de0b7fa29 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -33,6 +33,7 @@ import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; @@ -550,14 +551,7 @@ public void call(FirebaseAuth auth) throws Exception { .build(); MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(response) - .build(); - FirebaseApp.initializeApp(new FirebaseOptions.Builder() - .setCredentials(credentials) - .setProjectId("test-project-id") - .setHttpTransport(transport) - .build()); + FirebaseAuth auth = getRetryDisabledAuth(response); // Test for common HTTP error codes for (int code : ImmutableList.of(302, 400, 401, 404, 500)) { @@ -566,7 +560,7 @@ public void call(FirebaseAuth auth) throws Exception { response.setContent("{}"); response.setStatusCode(code); try { - operation.call(FirebaseAuth.getInstance()); + operation.call(auth); fail("No error thrown for HTTP error: " + code); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); @@ -584,7 +578,7 @@ public void call(FirebaseAuth auth) throws Exception { response.setContent("{\"error\": {\"message\": \"USER_NOT_FOUND\"}}"); response.setStatusCode(500); try { - operation.call(FirebaseAuth.getInstance()); + operation.call(auth); fail("No error thrown for HTTP error"); } catch (ExecutionException e) { assertTrue(e.getCause().toString(), e.getCause() instanceof FirebaseAuthException); @@ -615,16 +609,9 @@ public void testGetUserUnexpectedHttpError() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); response.setContent("{\"not\" json}"); response.setStatusCode(500); - MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(response) - .build(); - FirebaseApp.initializeApp(new FirebaseOptions.Builder() - .setCredentials(credentials) - .setProjectId("test-project-id") - .setHttpTransport(transport) - .build()); + FirebaseAuth auth = getRetryDisabledAuth(response); try { - FirebaseAuth.getInstance().getUserAsync("testuser").get(); + auth.getUserAsync("testuser").get(); fail("No error thrown for JSON error"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); @@ -1173,15 +1160,10 @@ public void testGenerateSignInWithEmailLinkWithSettings() throws Exception { @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(); + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("{\"error\": {\"message\": \"UNAUTHORIZED_DOMAIN\"}}") + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); FirebaseUserManager userManager = auth.getUserManager(); try { userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null); @@ -1194,15 +1176,10 @@ public void testHttpErrorWithCode() { @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(); + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("{}") + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); FirebaseUserManager userManager = auth.getUserManager(); try { userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null); @@ -1231,6 +1208,26 @@ private static TestResponseInterceptor initializeAppForUserManagement(String ... return interceptor; } + private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse response) { + final MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + final FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setProjectId("test-project-id") + .setHttpTransport(transport) + .build()); + return FirebaseAuth.builder() + .setFirebaseApp(app) + .setUserManager(new Supplier() { + @Override + public FirebaseUserManager get() { + return new FirebaseUserManager(app, transport.createRequestFactory()); + } + }) + .build(); + } + private static void checkUserRecord(UserRecord userRecord) { assertEquals("testuser", userRecord.getUid()); assertEquals("testuser@example.com", userRecord.getEmail()); From a039e051c9433eb26eafbde5c152fdcb5a4b80d9 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 10 Feb 2020 11:16:50 -0800 Subject: [PATCH 103/456] fix: Enabled automatic HTTP retries for FirebaseProjectManagement (#356) * Enabled automatic HTTP retries for FirebaseProjectManagement * Added some test cases * Added helper function for disabling exponential backoff during tests * Added util class for simplifying retry tests * Simplified test cases * Removed unused method --- .../firebase/internal/ApiClientUtils.java | 18 +++- .../FirebaseProjectManagementServiceImpl.java | 25 +++-- .../firebase/internal/ApiClientUtilsTest.java | 13 +++ .../firebase/internal/TestApiClientUtils.java | 95 +++++++++++++++++++ ...ebaseProjectManagementServiceImplTest.java | 59 ++++++++++-- 5 files changed, 191 insertions(+), 19 deletions(-) create mode 100644 src/test/java/com/google/firebase/internal/TestApiClientUtils.java diff --git a/src/main/java/com/google/firebase/internal/ApiClientUtils.java b/src/main/java/com/google/firebase/internal/ApiClientUtils.java index 7506ff8ee..36ccf5cc8 100644 --- a/src/main/java/com/google/firebase/internal/ApiClientUtils.java +++ b/src/main/java/com/google/firebase/internal/ApiClientUtils.java @@ -29,7 +29,7 @@ */ public class ApiClientUtils { - private static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.builder() + static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.builder() .setMaxRetries(4) .setRetryStatusCodes(ImmutableList.of(500, 503)) .setMaxIntervalMillis(60 * 1000) @@ -43,9 +43,21 @@ public class ApiClientUtils { * @return A new {@code HttpRequestFactory} instance. */ public static HttpRequestFactory newAuthorizedRequestFactory(FirebaseApp app) { + return newAuthorizedRequestFactory(app, DEFAULT_RETRY_CONFIG); + } + + /** + * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and + * automatic retries. + * + * @param app {@link FirebaseApp} from which to obtain authorization credentials. + * @param retryConfig {@link RetryConfig} instance or null to disable retries. + * @return A new {@code HttpRequestFactory} instance. + */ + public static HttpRequestFactory newAuthorizedRequestFactory( + FirebaseApp app, @Nullable RetryConfig retryConfig) { HttpTransport transport = app.getOptions().getHttpTransport(); - return transport.createRequestFactory( - new FirebaseRequestInitializer(app, DEFAULT_RETRY_CONFIG)); + return transport.createRequestFactory(new FirebaseRequestInitializer(app, retryConfig)); } public static HttpRequestFactory newUnauthorizedRequestFactory(FirebaseApp app) { diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java index 72c570c6d..8abced696 100644 --- a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java +++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.util.Base64; import com.google.api.client.util.Key; @@ -32,8 +33,8 @@ import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.internal.ApiClientUtils; 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; @@ -55,6 +56,7 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS private final FirebaseApp app; private final Sleeper sleeper; private final Scheduler scheduler; + private final HttpRequestFactory requestFactory; private final HttpHelper httpHelper; private final CreateAndroidAppFromAppIdFunction createAndroidAppFromAppIdFunction = @@ -63,17 +65,26 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS new CreateIosAppFromAppIdFunction(); FirebaseProjectManagementServiceImpl(FirebaseApp app) { - this(app, Sleeper.DEFAULT, new FirebaseAppScheduler(app)); + this( + app, + Sleeper.DEFAULT, + new FirebaseAppScheduler(app), + ApiClientUtils.newAuthorizedRequestFactory(app)); } - FirebaseProjectManagementServiceImpl(FirebaseApp app, Sleeper sleeper, Scheduler scheduler) { + @VisibleForTesting + FirebaseProjectManagementServiceImpl( + FirebaseApp app, Sleeper sleeper, Scheduler scheduler, HttpRequestFactory requestFactory) { this.app = checkNotNull(app); this.sleeper = checkNotNull(sleeper); this.scheduler = checkNotNull(scheduler); - this.httpHelper = new HttpHelper( - app.getOptions().getJsonFactory(), - app.getOptions().getHttpTransport().createRequestFactory( - new FirebaseRequestInitializer(app))); + this.requestFactory = checkNotNull(requestFactory); + this.httpHelper = new HttpHelper(app.getOptions().getJsonFactory(), requestFactory); + } + + @VisibleForTesting + HttpRequestFactory getRequestFactory() { + return requestFactory; } @VisibleForTesting diff --git a/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java index 78eabaf32..c19f5f567 100644 --- a/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java +++ b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java @@ -69,6 +69,19 @@ public void testAuthorizedHttpClient() throws IOException { assertEquals(retryConfig.getRetryStatusCodes(), ImmutableList.of(500, 503)); } + @Test + public void testAuthorizedHttpClientWithoutRetry() throws IOException { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); + + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app, null); + + assertTrue(requestFactory.getInitializer() instanceof FirebaseRequestInitializer); + HttpRequest request = requestFactory.buildGetRequest(TEST_URL); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + assertFalse(retryHandler instanceof RetryHandlerDecorator); + } + @Test public void testUnauthorizedHttpClient() throws IOException { FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); diff --git a/src/test/java/com/google/firebase/internal/TestApiClientUtils.java b/src/test/java/com/google/firebase/internal/TestApiClientUtils.java new file mode 100644 index 000000000..9f4bc2d09 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/TestApiClientUtils.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static com.google.firebase.internal.ApiClientUtils.DEFAULT_RETRY_CONFIG; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +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.HttpUnsuccessfulResponseHandler; +import com.google.api.client.testing.util.MockSleeper; +import com.google.firebase.FirebaseApp; +import com.google.firebase.internal.RetryInitializer.RetryHandlerDecorator; +import java.io.IOException; + +public class TestApiClientUtils { + + private static final RetryConfig TEST_RETRY_CONFIG = RetryConfig.builder() + .setMaxRetries(DEFAULT_RETRY_CONFIG.getMaxRetries()) + .setRetryStatusCodes(DEFAULT_RETRY_CONFIG.getRetryStatusCodes()) + .setMaxIntervalMillis(DEFAULT_RETRY_CONFIG.getMaxIntervalMillis()) + .setSleeper(new MockSleeper()) + .build(); + + private static final GenericUrl TEST_URL = new GenericUrl("https://firebase.google.com"); + + /** + * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and + * automatic retries. Bypasses exponential backoff between consecutive retries for faster + * execution during tests. + * + * @param app {@link FirebaseApp} from which to obtain authorization credentials. + * @return A new {@code HttpRequestFactory} instance. + */ + public static HttpRequestFactory delayBypassedRequestFactory(FirebaseApp app) { + return ApiClientUtils.newAuthorizedRequestFactory(app, TEST_RETRY_CONFIG); + } + + /** + * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts but + * no retries. + * + * @param app {@link FirebaseApp} from which to obtain authorization credentials. + * @return A new {@code HttpRequestFactory} instance. + */ + public static HttpRequestFactory retryDisabledRequestFactory(FirebaseApp app) { + return ApiClientUtils.newAuthorizedRequestFactory(app, null); + } + + /** + * Checks whther the given HttpRequestFactory has been configured for authorization and + * automatic retries. + * + * @param requestFactory The HttpRequestFactory to check. + */ + public static void assertAuthAndRetrySupport(HttpRequestFactory requestFactory) { + assertTrue(requestFactory.getInitializer() instanceof FirebaseRequestInitializer); + HttpRequest request; + try { + request = requestFactory.buildGetRequest(TEST_URL); + } catch (IOException e) { + throw new RuntimeException("Failed to initialize request", e); + } + + // Verify authorization + assertTrue(request.getHeaders().getAuthorization().startsWith("Bearer ")); + + // Verify retry support + HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + assertTrue(retryHandler instanceof RetryHandlerDecorator); + RetryConfig retryConfig = ((RetryHandlerDecorator) retryHandler).getRetryHandler() + .getRetryConfig(); + assertEquals(DEFAULT_RETRY_CONFIG.getMaxRetries(), retryConfig.getMaxRetries()); + assertEquals(DEFAULT_RETRY_CONFIG.getMaxIntervalMillis(), retryConfig.getMaxIntervalMillis()); + assertFalse(retryConfig.isRetryOnIOExceptions()); + assertEquals(DEFAULT_RETRY_CONFIG.getRetryStatusCodes(), retryConfig.getRetryStatusCodes()); + } +} diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java index a2fe8728d..87afe609c 100644 --- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java @@ -29,6 +29,7 @@ import com.google.api.client.googleapis.util.Utils; 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.json.JsonParser; @@ -43,6 +44,7 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.TestApiClientUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -370,7 +372,7 @@ public void listIosAppsAsyncMultiplePages() throws Exception { MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); secondRpcResponse.setContent(LIST_IOS_APPS_PAGE_2_RESPONSE); serviceImpl = initServiceImpl( - ImmutableList.of(firstRpcResponse, secondRpcResponse), + ImmutableList.of(firstRpcResponse, secondRpcResponse), interceptor); List iosAppList = serviceImpl.listIosAppsAsync(PROJECT_ID).get(); @@ -400,7 +402,7 @@ public void createIosApp() throws Exception { MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse(); thirdRpcResponse.setContent(CREATE_IOS_GET_OPERATION_ATTEMPT_2_RESPONSE); serviceImpl = initServiceImpl( - ImmutableList.of( + ImmutableList.of( firstRpcResponse, secondRpcResponse, thirdRpcResponse), interceptor); @@ -624,7 +626,7 @@ public void listAndroidAppsMultiplePages() throws Exception { MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); secondRpcResponse.setContent(LIST_ANDROID_APPS_PAGE_2_RESPONSE); serviceImpl = initServiceImpl( - ImmutableList.of(firstRpcResponse, secondRpcResponse), + ImmutableList.of(firstRpcResponse, secondRpcResponse), interceptor); List androidAppList = serviceImpl.listAndroidApps(PROJECT_ID); @@ -652,7 +654,7 @@ public void listAndroidAppsAsyncMultiplePages() throws Exception { MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); secondRpcResponse.setContent(LIST_ANDROID_APPS_PAGE_2_RESPONSE); serviceImpl = initServiceImpl( - ImmutableList.of(firstRpcResponse, secondRpcResponse), + ImmutableList.of(firstRpcResponse, secondRpcResponse), interceptor); List androidAppList = serviceImpl.listAndroidAppsAsync(PROJECT_ID).get(); @@ -682,7 +684,7 @@ public void createAndroidApp() throws Exception { MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse(); thirdRpcResponse.setContent(CREATE_ANDROID_GET_OPERATION_ATTEMPT_2_RESPONSE); serviceImpl = initServiceImpl( - ImmutableList.of( + ImmutableList.of( firstRpcResponse, secondRpcResponse, thirdRpcResponse), interceptor); @@ -714,7 +716,7 @@ public void createAndroidAppAsync() throws Exception { MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse(); thirdRpcResponse.setContent(CREATE_ANDROID_GET_OPERATION_ATTEMPT_2_RESPONSE); serviceImpl = initServiceImpl( - ImmutableList.of( + ImmutableList.of( firstRpcResponse, secondRpcResponse, thirdRpcResponse), interceptor); @@ -915,10 +917,48 @@ public void deleteShaCertificateAsync() throws Exception { checkRequestHeader(expectedUrl, HttpMethod.DELETE); } + @Test + public void testAuthAndRetriesSupport() { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId(PROJECT_ID) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + + FirebaseProjectManagementServiceImpl serviceImpl = + new FirebaseProjectManagementServiceImpl(app); + + TestApiClientUtils.assertAuthAndRetrySupport(serviceImpl.getRequestFactory()); + } + + @Test + public void testHttpRetries() throws Exception { + List mockResponses = ImmutableList.of( + firstRpcResponse.setStatusCode(503).setContent("{}"), + new MockLowLevelHttpResponse().setContent("{}")); + 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); + HttpRequestFactory requestFactory = TestApiClientUtils.delayBypassedRequestFactory(app); + FirebaseProjectManagementServiceImpl serviceImpl = new FirebaseProjectManagementServiceImpl( + app, new MockSleeper(), new MockScheduler(), requestFactory); + serviceImpl.setInterceptor(interceptor); + + serviceImpl.deleteShaCertificate(SHA1_RESOURCE_NAME); + + 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); + return initServiceImpl(ImmutableList.of(mockResponse), interceptor); } private static FirebaseProjectManagementServiceImpl initServiceImpl( @@ -931,8 +971,9 @@ private static FirebaseProjectManagementServiceImpl initServiceImpl( .setHttpTransport(transport) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); - FirebaseProjectManagementServiceImpl serviceImpl = - new FirebaseProjectManagementServiceImpl(app, new MockSleeper(), new MockScheduler()); + HttpRequestFactory requestFactory = TestApiClientUtils.retryDisabledRequestFactory(app); + FirebaseProjectManagementServiceImpl serviceImpl = new FirebaseProjectManagementServiceImpl( + app, new MockSleeper(), new MockScheduler(), requestFactory); serviceImpl.setInterceptor(interceptor); return serviceImpl; } From c9648cfb5a1d6d789ffd2535331677bd6c6502b1 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 11 Feb 2020 14:23:02 -0500 Subject: [PATCH 104/456] Staged Release 6.12.2 (#364) * [maven-release-plugin] prepare release v6.12.2 * [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e82439e48..665edc8a6 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.12.2-SNAPSHOT + 6.12.3-SNAPSHOT jar firebase-admin From c40a69d04c73e158a0ea1e75b1ea7400c3544c92 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 18 Feb 2020 10:11:27 -0800 Subject: [PATCH 105/456] chore: Implementing GitHub Actions release workflow (#363) * Adding release workflow preliminaries * Fixed a syntax error * Uploading only the jar artifacts * Enabling integration tests * Using Maven lifecycle more effectively * Fixing indentation * Maven deployment configuration * Point the send tweet action to master * Setting all publish config as env variables * Fixed typo in tweet --- .github/resources/firebase.asc.gpg | Bin 0 -> 4056 bytes .../resources/integ-service-account.json.gpg | Bin 0 -> 1734 bytes .github/resources/settings.xml | 29 ++++ .github/scripts/generate_changelog.sh | 79 ++++++++++ .github/scripts/package_artifacts.sh | 36 +++++ .github/scripts/publish_artifacts.sh | 35 +++++ .github/scripts/publish_preflight_check.sh | 146 ++++++++++++++++++ .github/workflows/ci.yml | 24 ++- .github/workflows/release.yml | 129 ++++++++++++++++ pom.xml | 119 +++++++------- 10 files changed, 532 insertions(+), 65 deletions(-) create mode 100644 .github/resources/firebase.asc.gpg create mode 100644 .github/resources/integ-service-account.json.gpg create mode 100644 .github/resources/settings.xml create mode 100755 .github/scripts/generate_changelog.sh create mode 100755 .github/scripts/package_artifacts.sh create mode 100755 .github/scripts/publish_artifacts.sh create mode 100755 .github/scripts/publish_preflight_check.sh create mode 100644 .github/workflows/release.yml diff --git a/.github/resources/firebase.asc.gpg b/.github/resources/firebase.asc.gpg new file mode 100644 index 0000000000000000000000000000000000000000..a946776c7fd1498a0399836a94785dc8fd5293ed GIT binary patch literal 4056 zcmV;}4=3=94Fm}T0y&=z%W?c1O8?U90o%rSLxzz3Cs`6<0^{*c`lR7N#ikp)!>DCQ z2so>P8Xz5P`%krULZ%hwWtW!=gEFCn@5ZoJkLvVsjQj?_gTijiyjQd;s;+?fEMEIl zCW_+-tHe1(pA!HtoTeq|lxrM{iU@)90oap)GEIqUB-CIKkMVwgc7A5iMm|VXlX9c;YSx z3RIqP!LqF6ofr8ell_En&(EMvLLs1qA4|f=kV=jbPQ76z_u0iIsBI((a|_S(VY-`*o&I+#7og2mvd1bVY^0 zA*7F*e;zlI^~iUzzhHPPLG^UG8LnIYHnC=D_HwMyZ8+%J>EEXHy*$CCin!zIgn)r& zeHOTG)WpHuyy3PDf7ogl1~I`lrUe)%qVtb$eDdDeZ@A8iku6Tjet@7P)bfU7#guou zNb?bve9@1LDToX%)Ev253ot{ZD&&1G#`Y`jLmP#YO>Q=2tLod!NV>DwuooOnEEYwd z%wu*~D`sKW5)K2S;}>U+y_=@mAnxRQ6opOoWzm#mRSppcyD!tUjZM|;Px{@dP zBG{0!Sh6P!W{N>7`Iv7y{~m4XVvzfBP=^b<7s5>mK*)n!RHgfXGw_f31aJ&4N^sd} zKi}q$0l3trF9dJx;9_Px#Vj?A_#0mF_z0|j*R$(DO7Xr zZpNqdnM~+ZY>e@$;S(kIV-V^a@J8BL)H&)4@&9HzfXT=Il+j@CSI@?zoDiESCgN>^ z#pHteO6U&MCvWmwSq8|;RRUvm<3^0BE|>K0i~@WuuaYpOz&I*9xh8}|@dg%V&CF|L zjFb)OOOe8-KaVW=c#S`Ss}RlrOZ?#O*e$Zfq;e%ryTPGShAnu~lCvuHZMrihx7xlx zOldQSRd0@j9j--aCzT7^QKEl=V$5g~I@@Wwp>jI3>!f7QuG#g>wSdkhT2p?%dzVVg5laj~V|q>A|JFUL}b98ib12!uE-b?Bau8(pUW}a91l>SVGGx;XOV)6025jq&C z=twVclr4(}0I?N&^aaVxtCBQCR))pC@EqriFb%+-DI5CrhP_PhAa9Y7I)@w zAM_59o0Qvqd3}Z+V6=o`M>WWVXE+&!?nF zLvnv8yA!_8okUw9?3-ONUs2IYTt}$V2|G^fgO<7OMVu!PXI9%Et z5s1`~8iy6(0=AAfa8|uITcKp490(SSVaum>4=Jh>ronEnW>OrY9wJ!mAe9Nte2*on?U zT?a*nuNb5(!8XBRAX&Flbv`2-6JPU)+QZd~>Y47db7Jx#eK<&zabmE98=cxKS(4eH z`~LxwSvU7Ve(gS#jSZH>(QfE86u7?#JTl6+$H~qAfD&Azhw=VtmZGh1f}l5yRriKO zX3M|cmHms6?b7brMTiPh)v#(~tj$p{+VI59Ahkf0I%tVP+Xj?1R=dnULjCiSmB;Cc zzX@;6nt{_4HjG2s=ocb|Z(~geFdCvH!qGK3{H6$Tt;%08lj+{Xb`OMw=DGkjlV{h_ zx9iiwLi3&5l>!iJ$*7<_?7dQnf`W4u`o2_78XQrmKSF^Z!L(q$2Cx*)8^bN}x$4h2 zPc!gA|Ee6=3VN*$WfOlPn2oiNcVVv4l}{Lag$6_g#i{+wc3b{JPE})$0Ls`Ly8NKx zaLzE8Ox77z1_sYV%zeTx%^~OcbNhtxQXGc!?+7{iV6Y1^7Gz;g&;d;w>0lgUe22E~ zroyyWSlW?m69Sv_H8w6(} z+xj5L1e3|mf@0}hxL)4wM&~%rV~a8p(g*_onDrC7%H3ckPbodKsE&y}hUW^iPB0YS zKQVl&_8p>?Iz|#3+QB^u4(nN<>I+5Os+4wTYOmAEP0i5wS%O$RFCHYuBN(C%R6Hs; zOe)l~$Ku1MN0fcI8&0cra9jplsOyUl3?+lOylU)YyUv8f2az|~#pXg{P<%~TSPV&% zyM^15lsNqI$ClcwXIjr;vmG5Alzg23T>^XwXFP=lXA|xipmA^L=c_Yb5tug$9cuS$ z25^~Q8azMB>oq7o#U-|4SIq)>HQp2Iy!uc>s#*_X71mG*Qm32k%8h<9w0}0LhcGd3 zxYyQGK+!}J1_V$Y>EobOVhcOY!xcBR5CA}j2WC-@<=w9j)-`uUaj#DLF!_HznM#Lz zY%1K9k61k8JS!~W!OCJ@!r+fds8CMcc^4VVHOp*umI1e0un{e=lkW@3-8A6{H$63bSEi^yFI;=q3{WtI|mI_}hmX5=8hD3!XCA4jA)ty$VeRhT!cfcTUV+ zK3XpIA(E-?V%<@jAurSB3lw2Ip%BO>p|Tu2=1Eez7S{=0wQmt8Yv{KmyNz(#8^qlq zW7rZ}3i8jb5eukdrQv0!PQOcOhev=~y-T+%1Mv+VJr2tJ7Mu$PMenudb8iF(prngJ zIEuBG;~hkP?>IDcTO@3vUC#;HCv$Sv!VdM#5&0>P-v&(K2e2Hzx{K*a>fXmcR1wqe3Z8{5+CFGDx5l!PvCe#wMNn%eApGB0;`?EY{JZHY`28~L zB=22wfOTv01{hJNXn8(G!PWNTuNq|dB)6xI$2^`NHro7vTmPmbFM<^KefX_TSPTg; zl*h&qo-dZ+$%a5yUNi+tbm(|zu={)Do3<1A4CNiurj6(It`Frh*sGKl=1UQN^^E^I z9FAjNimgI(YM*F+B6aANlQ&8Z9gXhok+j^{6zSo(cN8!i{KS9X+s{!r#x^7@kbth) z$+b5ul8vGL{J!_#OB^uHd&4K3dBk7rB~J$)fo;OFm%@aVAGE(ZPCZ1PrH(KtMo1~t z+Y9X5?o;j~Z5MN=C*A$7-a(UHc|fPyuFh^e!{~wJAJ9AzO;jj^hX#tL2Y@Me2++(m z){lC!c;^egc?B^a)Fla4>M3YTZ#5b=&OciVJCQDF<h~nMeWb9n*u6nmnT!UW{+K-Fpba>kTBsChG!vmLMk7CNGIo3=d-`YOBWZ@wT1{K^ zD0*+k1^Uy=_fL1&o()tA>K3E944{K;J8f6c=M9sXM?|H>?rUP&o0@7=QYB5|+Q{s^ zu4p?+s8d$@zcO6->L*;nGJXL_Y3Sv^_yF z=G;x4p%gDB0zUvhxk#G?iGD8Yk5dy6yGI5VlHX&d@l4ebjr;x8?iHtp&U%4@ZLjf~ zt%J+l3NK6v&&7VEwTV&Ely+pvh~5-Cs&qAo&5S*YA8?Jj?X}*PKE{0@b!=_Sg2^Ux zY{w0(nR)c$p3~SsxwI^&uCV}%z7oL$+ciBkJZ5aR0UA%cw-&;vy$|~_jFK`EQwP+( z4KwsI@xrjC-9h$FmJg-36Om%AG}fRW`H*+hfb)XOloW1Z3L7f{ub%JdpIJUgPA-bq6BG|q_Fs-h;T z>yDbjZIOs91!|4^+n`~dYA4JH+1LBHCNjabPA+o& z9XgjmGBm>#@WC!hDn9~uqaId;^A`Ny^~BNK8nF713ReS%2Grj5^oShCV@jF}dL?^s z*gf`7^qQ2G6bNwg(Sz&

      z4r*r|VPWM6hN{&7xv$RQO}Zn7RnRZ!)V9sRB$hC;}* KX);zY-!1|?!O3F) literal 0 HcmV?d00001 diff --git a/.github/resources/integ-service-account.json.gpg b/.github/resources/integ-service-account.json.gpg new file mode 100644 index 0000000000000000000000000000000000000000..da547f44dff331d7c4b1e454ca1a230aef03622c GIT binary patch literal 1734 zcmV;%208hR4Fm}T0_WqJMLyD96#vrd0kSseMv-_5+A>|dXkh)Lxg1WlZ_;HhFTh#x zDXB$vd955)y_i6fJ)Oj)aBCs+sb^rD8ckG^9BEnrFn(oz#yfzS@$+cn3lx!+B3v>8 z+oZYA$+d_~HR_?8Y|47V&47`mfrj&cS28OWOy@;W6RABVdPnTBDiQppT7W_J!vZ~k zJU+n4wlc$u!0!vj)k)conSKAdiLj5M4~3gTnMvFiA3OAaxG9$-Fi4Hz8zrK}UW+wP zy2KkBMs*nJt{;S3op3>^&flbtsElr~I!hsD^U2aEDCp+ScE}*!0)iJIYxI~0rz0!! z$*UJ{b*NbH9yB>mQVu(OxgZzVjskCoA1LLO)@Ob*I`%xmx3pY(UfU8<8kU$joj5L4 zATtgXL$nQ}etB%PN4yHDL~%HN5EDvD5vvXO-mjxu`Jw^zi}3M^AJM(=3}3DOf8t`1 zj@}P~D3y#8&Z~?x@1we($j}oT5e)y|907FiLk@uC)}}$TRV@Z^%|3?~AHf{N=JdS& zmAzu?{0*volC=e78`s^b_7LW?R+;1;+(&V(( zT*de9#KZNX^&6OE$fSpA*AbRQ@qfp9pz}Jz&>w6bXOw_#xf2&v-L&Phl|oHWm@D3} zOys2V$RMQlp$0{v*s34|sV=iP;x;zP=5RpwWJqnMP&QI7t)^63FDPCOen|MjFf6Fy zbH8(4!{wkLz8MP^38`HGosmm`E%%?rxE~2g)rNC_O(^3S*C5=3vYzG?3+y!oJ{s{1 zzUNA>f{FX&_8Zr~`3Aw+iI_(l<`oeGJGikkiq2sFOWR;=B!0NQRSX2%#^zb?1Vrn0 zUAk&@PC~v?RWOgIT?*LH&uK#GKtFXuFkP#zy6ByVhMXCsN=h$QNcx6Fab!8QW6Ur& zu}^KvHWAtV9#3GkF0lWTE~;FldqZH>&o2Z%KX~iA^p+fyiGvth=T-)<@CAc* z?GOwo%qlyt!74iU48M9XWmg)7k`fK)b}RNsiVo+UFE1g{y{tK^E0QF7xiW`>{iE_L z2e)`aCjrF4vaJ(AvkPqelNNjHn&uE@P7P07O+*CceK`8`Ej^O>;!(i7HaCaidZ~kj z$!=OMn@HL1CK6=ew?$zQygRGVm+>k2DR7{CKke27evSnGj z1DV~gv?jynB1yH$8kV3HDk^T{2PYCrSXN1{hh09 z;l&(GhZ7|l=@6E5%7=&;~1HUa3Edum39CeKllCDf5wr&+x@&+?!$Me2rKSZQ|DnUwYV~xd6kbA z^fr&FN-_FU?HQ=YD~>e6BR*{U((QLQmN58HK)d%N*5aFTL7;s!iBET0tq(c}0dVe8m%YTz{Xp6^}&9rC_ zFW>egRI>>@J|qb&;qSrsBP~vN2r{|}xny7Ft7$IqnF5kN{Ynzk**xBl6FNa!ASKZr zjkC^DnaK9M?q75@{PckJ6UMReK^Nr?iNe=Y6Rz(Imio@B{ax^h!#Hx-I^eU|&f@Sk zeh`^4eBkBo*Y~t$e?2{Lb0)LXxo?T=3jk#6n!+oCl3nSeNpa@uI5OjP zK#5K-d&uK-Mex4OqRTzG{P7!FFn+a literal 0 HcmV?d00001 diff --git a/.github/resources/settings.xml b/.github/resources/settings.xml new file mode 100644 index 000000000..708bbcb75 --- /dev/null +++ b/.github/resources/settings.xml @@ -0,0 +1,29 @@ + + + + false + + + + ossrh + ${env.NEXUS_OSSRH_USERNAME} + ${env.NEXUS_OSSRH_PASSWORD} + + + + + + release + + true + + + gpg + B652FFD3865AF7A75830876F5F55C8F6985BB9DD + ${env.GPG_PASSPHRASE} + + + + diff --git a/.github/scripts/generate_changelog.sh b/.github/scripts/generate_changelog.sh new file mode 100755 index 000000000..e393f40e4 --- /dev/null +++ b/.github/scripts/generate_changelog.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# Copyright 2020 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. + +set -e +set -u + +function printChangelog() { + local TITLE=$1 + shift + # Skip the sentinel value. + local ENTRIES=("${@:2}") + if [ ${#ENTRIES[@]} -ne 0 ]; then + echo "### ${TITLE}" + echo "" + for ((i = 0; i < ${#ENTRIES[@]}; i++)) + do + echo "* ${ENTRIES[$i]}" + done + echo "" + fi +} + +if [[ -z "${GITHUB_SHA}" ]]; then + GITHUB_SHA="HEAD" +fi + +LAST_TAG=`git describe --tags $(git rev-list --tags --max-count=1) 2> /dev/null` || true +if [[ -z "${LAST_TAG}" ]]; then + echo "[INFO] No tags found. Including all commits up to ${GITHUB_SHA}." + VERSION_RANGE="${GITHUB_SHA}" +else + echo "[INFO] Last release tag: ${LAST_TAG}." + COMMIT_SHA=`git show-ref -s ${LAST_TAG}` + echo "[INFO] Last release commit: ${COMMIT_SHA}." + VERSION_RANGE="${COMMIT_SHA}..${GITHUB_SHA}" + echo "[INFO] Including all commits in the range ${VERSION_RANGE}." +fi + +echo "" + +# Older versions of Bash (< 4.4) treat empty arrays as unbound variables, which triggers +# errors when referencing them. Therefore we initialize each of these arrays with an empty +# sentinel value, and later skip them. +CHANGES=("") +FIXES=("") +FEATS=("") +MISC=("") + +while read -r line +do + COMMIT_MSG=`echo ${line} | cut -d ' ' -f 2-` + if [[ $COMMIT_MSG =~ ^change(\(.*\))?: ]]; then + CHANGES+=("$COMMIT_MSG") + elif [[ $COMMIT_MSG =~ ^fix(\(.*\))?: ]]; then + FIXES+=("$COMMIT_MSG") + elif [[ $COMMIT_MSG =~ ^feat(\(.*\))?: ]]; then + FEATS+=("$COMMIT_MSG") + else + MISC+=("${COMMIT_MSG}") + fi +done < <(git log ${VERSION_RANGE} --oneline) + +printChangelog "Breaking Changes" "${CHANGES[@]}" +printChangelog "New Features" "${FEATS[@]}" +printChangelog "Bug Fixes" "${FIXES[@]}" +printChangelog "Miscellaneous" "${MISC[@]}" diff --git a/.github/scripts/package_artifacts.sh b/.github/scripts/package_artifacts.sh new file mode 100755 index 000000000..6e993066e --- /dev/null +++ b/.github/scripts/package_artifacts.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Copyright 2020 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. + +set -e +set -u + +gpg --quiet --batch --yes --decrypt --passphrase="${FIREBASE_SERVICE_ACCT_KEY}" \ + --output integration_cert.json .github/resources/integ-service-account.json.gpg + +echo "${FIREBASE_API_KEY}" > integration_apikey.txt + +# Does the following: +# 1. Runs the Checkstyle plugin (validate phase) +# 2. Compiles the source (compile phase) +# 3. Runs the unit tests (test phase) +# 4. Packages the artifacts - src, bin, javadocs (package phase) +# 5. Runs the integration tests (verify phase) +mvn -B clean verify + +# Maven target directory can consist of many files. Just copy the jar artifacts +# into a new directory for upload. +mkdir -p dist +cp target/*.jar dist/ diff --git a/.github/scripts/publish_artifacts.sh b/.github/scripts/publish_artifacts.sh new file mode 100755 index 000000000..f4a2f1734 --- /dev/null +++ b/.github/scripts/publish_artifacts.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Copyright 2020 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. + +set -e +set -u + +gpg --quiet --batch --yes --decrypt --passphrase="${GPG_PRIVATE_KEY}" \ + --output firebase.asc .github/resources/firebase.asc.gpg + +gpg --import firebase.asc + +# Does the following: +# 1. Compiles the source (compile phase) +# 2. Packages the artifacts - src, bin, javadocs (package phase) +# 3. Signs the artifacts (verify phase) +# 4. Publishes artifacts via Nexus (deploy phase) +mvn -B clean deploy \ + -Dcheckstyle.skip \ + -DskipTests \ + -Prelease \ + --settings .github/resources/settings.xml + diff --git a/.github/scripts/publish_preflight_check.sh b/.github/scripts/publish_preflight_check.sh new file mode 100755 index 000000000..7a191518d --- /dev/null +++ b/.github/scripts/publish_preflight_check.sh @@ -0,0 +1,146 @@ +#!/bin/bash + +# Copyright 2020 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. + + +###################################### Outputs ##################################### + +# 1. version: The version of this release including the 'v' prefix (e.g. v1.2.3). +# 2. changelog: Formatted changelog text for this release. + +#################################################################################### + +set -e +set -u + +function echo_info() { + local MESSAGE=$1 + echo "[INFO] ${MESSAGE}" +} + +function echo_warn() { + local MESSAGE=$1 + echo "[WARN] ${MESSAGE}" +} + +function terminate() { + echo "" + echo_warn "--------------------------------------------" + echo_warn "PREFLIGHT FAILED" + echo_warn "--------------------------------------------" + exit 1 +} + + +echo_info "Starting publish preflight check..." +echo_info "Git revision : ${GITHUB_SHA}" +echo_info "Workflow triggered by : ${GITHUB_ACTOR}" +echo_info "GitHub event : ${GITHUB_EVENT_NAME}" + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Extracting release version" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "Loading version from: pom.xml" +readonly RELEASE_VERSION=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout` || true +if [[ -z "${RELEASE_VERSION}" ]]; then + echo_warn "Failed to extract release version from: pom.xml" + terminate +fi + +if [[ ! "${RELEASE_VERSION}" =~ ^([0-9]*)\.([0-9]*)\.([0-9]*)$ ]]; then + echo_warn "Malformed release version string: ${RELEASE_VERSION}. Exiting." + terminate +fi + +echo_info "Extracted release version: ${RELEASE_VERSION}" +echo "::set-output name=version::v${RELEASE_VERSION}" + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Checking previous releases" +echo_info "--------------------------------------------" +echo_info "" + +readonly MAVEN_CENTRAL_URL="https://repo1.maven.org/maven2/com/google/firebase/firebase-admin/${RELEASE_VERSION}" +readonly MAVEN_STATUS=`curl -s -o /dev/null -L -w "%{http_code}" ${MAVEN_CENTRAL_URL}` +if [[ $MAVEN_STATUS -eq 404 ]]; then + echo_info "Release version ${RELEASE_VERSION} not found in Maven Central." +elif [[ $MAVEN_STATUS -eq 200 ]]; then + echo_warn "Release version ${RELEASE_VERSION} already present in Maven Central." + terminate +else + echo_warn "Unexpected ${MAVEN_STATUS} response from Maven Central. Exiting." + terminate +fi + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Checking release tag" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "---< git fetch --depth=1 origin +refs/tags/*:refs/tags/* >---" +git fetch --depth=1 origin +refs/tags/*:refs/tags/* +echo "" + +readonly EXISTING_TAG=`git rev-parse -q --verify "refs/tags/v${RELEASE_VERSION}"` || true +if [[ -n "${EXISTING_TAG}" ]]; then + echo_warn "Tag v${RELEASE_VERSION} already exists. Exiting." + echo_warn "If the tag was created in a previous unsuccessful attempt, delete it and try again." + echo_warn " $ git tag -d v${RELEASE_VERSION}" + echo_warn " $ git push --delete origin v${RELEASE_VERSION}" + + readonly RELEASE_URL="https://github.com/firebase/firebase-admin-java/releases/tag/v${RELEASE_VERSION}" + echo_warn "Delete any corresponding releases at ${RELEASE_URL}." + terminate +fi + +echo_info "Tag v${RELEASE_VERSION} does not exist." + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Generating changelog" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "---< git fetch origin master --prune --unshallow >---" +git fetch origin master --prune --unshallow +echo "" + +echo_info "Generating changelog from history..." +readonly CURRENT_DIR=$(dirname "$0") +readonly CHANGELOG=`${CURRENT_DIR}/generate_changelog.sh` +echo "$CHANGELOG" + +# Parse and preformat the text to handle multi-line output. +# See https://github.amrom.workers.devmunity/t5/GitHub-Actions/set-output-Truncates-Multiline-Strings/td-p/37870 +FILTERED_CHANGELOG=`echo "$CHANGELOG" | grep -v "\\[INFO\\]"` +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//'%'/'%25'}" +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\n'/'%0A'}" +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\r'/'%0D'}" +echo "::set-output name=changelog::${FILTERED_CHANGELOG}" + + +echo "" +echo_info "--------------------------------------------" +echo_info "PREFLIGHT SUCCESSFUL" +echo_info "--------------------------------------------" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10007c5c6..fbb1aad5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,15 +1,35 @@ +# Copyright 2020 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. name: Continuous Integration -on: [push, pull_request] +on: push jobs: build: runs-on: ubuntu-latest + steps: - uses: actions/checkout@v1 + - name: Set up JDK 1.7 uses: actions/setup-java@v1 with: java-version: 1.7 + + # Does the following: + # 1. Runs the Checkstyle plugin (validate phase) + # 2. Compiles the source (compile phase) + # 3. Runs the unit tests (test phase) - name: Build with Maven - run: mvn -B package --file pom.xml + run: mvn -B clean test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..0e1c307bc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,129 @@ +# Copyright 2020 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. + +name: Release Candidate + +on: + # Only run the workflow when a PR is updated or when a developer explicitly requests + # a build by sending a 'firebase_build' event. + pull_request: + types: [opened, synchronize, closed] + + repository_dispatch: + types: + - firebase_build + +jobs: + stage_release: + # To publish a release, merge the release PR with the label 'release:publish'. + # To stage a release without publishing it, send a 'firebase_build' event or apply + # the 'release:stage' label to a PR. + if: github.event.action == 'firebase_build' || + contains(github.event.pull_request.labels.*.name, 'release:stage') || + (github.event.pull_request.merged && + contains(github.event.pull_request.labels.*.name, 'release:publish')) + + runs-on: ubuntu-latest + + # When manually triggering the build, the requester can specify a target branch or a tag + # via the 'ref' client parameter. + steps: + - name: Checkout source for staging + uses: actions/checkout@v2 + with: + ref: ${{ github.event.client_payload.ref || github.ref }} + + - name: Set up JDK 1.7 + uses: actions/setup-java@v1 + with: + java-version: 1.7 + + - name: Compile, test and package + run: ./.github/scripts/package_artifacts.sh + env: + FIREBASE_SERVICE_ACCT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCT_KEY }} + FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} + + # Attach the packaged artifacts to the workflow output. These can be manually + # downloaded for later inspection if necessary. + - name: Archive artifacts + uses: actions/upload-artifact@v1 + with: + name: dist + path: dist + + publish_release: + needs: stage_release + + # Check whether the release should be published. We publish only when the trigger PR is + # 1. merged + # 2. to the master branch + # 3. with the label 'release:publish', and + # 4. the title prefix '[chore] Release '. + if: github.event.pull_request.merged && + github.ref == 'master' && + contains(github.event.pull_request.labels.*.name, 'release:publish') && + startsWith(github.event.pull_request.title, '[chore] Release ') + + runs-on: ubuntu-latest + + steps: + - name: Checkout source for publish + uses: actions/checkout@v2 + + - name: Set up JDK 1.7 + uses: actions/setup-java@v1 + with: + java-version: 1.7 + + - name: Publish preflight check + id: preflight + run: ./.github/scripts/publish_preflight_check.sh + + # We pull this action from a custom fork of a contributor until + # https://github.com/actions/create-release/pull/32 is merged. Also note that v1 of + # this action does not support the "body" parameter. + - name: Create release tag + uses: fleskesvor/create-release@1a72e235c178bf2ae6c51a8ae36febc24568c5fe + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.preflight.outputs.version }} + release_name: Firebase Admin Java SDK ${{ steps.preflight.outputs.version }} + body: ${{ steps.preflight.outputs.changelog }} + draft: false + prerelease: false + + - name: Publish to Maven Central + run: ./.github/scripts/publish_artifacts.sh + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + NEXUS_OSSRH_USERNAME: ${{ secrets.NEXUS_OSSRH_USERNAME }} + NEXUS_OSSRH_PASSWORD: ${{ secrets.NEXUS_OSSRH_PASSWORD }} + + # Post to Twitter if explicitly opted-in by adding the label 'release:tweet'. + - name: Post to Twitter + if: success() && + contains(github.event.pull_request.labels.*.name, 'release:tweet') + uses: firebase/firebase-admin-node/.github/actions/send-tweet@master + with: + status: > + ${{ steps.preflight.outputs.version }} of @Firebase Admin Java SDK is available. + https://github.com/firebase/firebase-admin-java/releases/tag/${{ steps.preflight.outputs.version }} + consumer-key: ${{ secrets.TWITTER_CONSUMER_KEY }} + consumer-secret: ${{ secrets.TWITTER_CONSUMER_SECRET }} + access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }} + access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} + continue-on-error: true diff --git a/pom.xml b/pom.xml index 665edc8a6..af25b87d7 100644 --- a/pom.xml +++ b/pom.xml @@ -161,56 +161,8 @@ release - - - true - - - maven-javadoc-plugin - - - package - - jar - - - - - - com.google.doclava - doclava - 1.0.6 - - com.google.doclava.Doclava - ${sun.boot.class.path} - - - com.google.j2objc - j2objc-annotations - 1.3 - - - - -warning 101 - - false - -J-Xmx1024m - - - - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - maven-gpg-plugin 1.5 @@ -295,6 +247,7 @@ + maven-checkstyle-plugin 2.17 @@ -315,6 +268,8 @@ + + maven-compiler-plugin 3.6.1 @@ -323,6 +278,8 @@ 1.7 + + maven-surefire-plugin 2.19.1 @@ -330,32 +287,68 @@ ${skipUTs} + + - maven-failsafe-plugin - 2.19.1 + maven-source-plugin + 2.2.1 + attach-sources - integration-test - verify + jar-no-fork - + maven-javadoc-plugin - 2.10.4 - - - maven-release-plugin - 2.5.3 + + + attach-javadocs + + jar + + + - false - release - v@{project.version} - deploy + + com.google.doclava + doclava + 1.0.6 + + com.google.doclava.Doclava + ${sun.boot.class.path} + + + com.google.j2objc + j2objc-annotations + 1.3 + + + + -warning 101 + + false + -J-Xmx1024m + + + + maven-failsafe-plugin + 2.19.1 + + + + integration-test + verify + + + + + + org.sonatype.plugins nexus-staging-maven-plugin @@ -364,7 +357,7 @@ ossrh https://oss.sonatype.org/ - false + true From b18cf69fc92a8a792879632ba296c4ff82a351d2 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 10 Apr 2020 13:53:47 -0700 Subject: [PATCH 106/456] chore: Updated CI workflow to run on all PRs (#390) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbb1aad5e..58ecbfa35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ # limitations under the License. name: Continuous Integration -on: push +on: pull_request jobs: build: From 2cfbc3d96d0090593828f78b497ea29c09fcaae1 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Fri, 10 Apr 2020 17:27:20 -0400 Subject: [PATCH 107/456] Testable BatchResponse (#389) * Testable BatchResponse - Converts batch response to an interface - Moves batch response implementation to BatchResponseImpl - Update FirebaseMessagingClientImpl to use BatchResponseImpl - Updated tests * Make BatchResponseImpl package private --- .../firebase/messaging/BatchResponse.java | 29 ++-------- .../firebase/messaging/BatchResponseImpl.java | 58 +++++++++++++++++++ .../FirebaseMessagingClientImpl.java | 2 +- .../firebase/messaging/BatchResponseTest.java | 8 +-- .../messaging/FirebaseMessagingTest.java | 2 +- 5 files changed, 68 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/google/firebase/messaging/BatchResponseImpl.java diff --git a/src/main/java/com/google/firebase/messaging/BatchResponse.java b/src/main/java/com/google/firebase/messaging/BatchResponse.java index bd5069f4c..164403be4 100644 --- a/src/main/java/com/google/firebase/messaging/BatchResponse.java +++ b/src/main/java/com/google/firebase/messaging/BatchResponse.java @@ -16,7 +16,6 @@ package com.google.firebase.messaging; -import com.google.common.collect.ImmutableList; import com.google.firebase.internal.NonNull; import java.util.List; @@ -25,32 +24,12 @@ * See {@link FirebaseMessaging#sendAll(List)} and {@link * FirebaseMessaging#sendMulticast(MulticastMessage)}. */ -public final class BatchResponse { - - private final List responses; - private final int successCount; - - BatchResponse(List responses) { - this.responses = ImmutableList.copyOf(responses); - int successCount = 0; - for (SendResponse response : this.responses) { - if (response.isSuccessful()) { - successCount++; - } - } - this.successCount = successCount; - } +public interface BatchResponse { @NonNull - public List getResponses() { - return responses; - } + List getResponses(); - public int getSuccessCount() { - return successCount; - } + int getSuccessCount(); - public int getFailureCount() { - return responses.size() - successCount; - } + int getFailureCount(); } diff --git a/src/main/java/com/google/firebase/messaging/BatchResponseImpl.java b/src/main/java/com/google/firebase/messaging/BatchResponseImpl.java new file mode 100644 index 000000000..99cf63df1 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/BatchResponseImpl.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 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 com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; + +import java.util.List; + +/** + * Response from an operation that sends FCM messages to multiple recipients. + * See {@link FirebaseMessaging#sendAll(List)} and {@link + * FirebaseMessaging#sendMulticast(MulticastMessage)}. + */ +class BatchResponseImpl implements BatchResponse { + + private final List responses; + private final int successCount; + + BatchResponseImpl(List responses) { + this.responses = ImmutableList.copyOf(responses); + int successCount = 0; + for (SendResponse response : this.responses) { + if (response.isSuccessful()) { + successCount++; + } + } + this.successCount = successCount; + } + + @NonNull + public List getResponses() { + return responses; + } + + public int getSuccessCount() { + return successCount; + } + + public int getFailureCount() { + return responses.size() - successCount; + } + +} diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java index c2659a270..53d5ab00b 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java @@ -167,7 +167,7 @@ private BatchResponse sendBatchRequest( MessagingBatchCallback callback = new MessagingBatchCallback(); BatchRequest batch = newBatchRequest(messages, dryRun, callback); batch.execute(); - return new BatchResponse(callback.getResponses()); + return new BatchResponseImpl(callback.getResponses()); } private BatchRequest newBatchRequest( diff --git a/src/test/java/com/google/firebase/messaging/BatchResponseTest.java b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java index cea001085..9c174f569 100644 --- a/src/test/java/com/google/firebase/messaging/BatchResponseTest.java +++ b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java @@ -31,7 +31,7 @@ public class BatchResponseTest { public void testEmptyResponses() { List responses = new ArrayList<>(); - BatchResponse batchResponse = new BatchResponse(responses); + BatchResponse batchResponse = new BatchResponseImpl(responses); assertEquals(0, batchResponse.getSuccessCount()); assertEquals(0, batchResponse.getFailureCount()); @@ -47,7 +47,7 @@ public void testSomeResponse() { "error-message", null)) ); - BatchResponse batchResponse = new BatchResponse(responses); + BatchResponse batchResponse = new BatchResponseImpl(responses); assertEquals(2, batchResponse.getSuccessCount()); assertEquals(1, batchResponse.getFailureCount()); @@ -61,7 +61,7 @@ public void testSomeResponse() { public void testResponsesImmutable() { List responses = new ArrayList<>(); responses.add(SendResponse.fromMessageId("message1")); - BatchResponse batchResponse = new BatchResponse(responses); + BatchResponse batchResponse = new BatchResponseImpl(responses); SendResponse sendResponse = SendResponse.fromMessageId("message2"); try { @@ -74,6 +74,6 @@ public void testResponsesImmutable() { @Test(expected = NullPointerException.class) public void testResponsesCannotBeNull() { - new BatchResponse(null); + new BatchResponseImpl(null); } } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index bebba0864..496823175 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -668,7 +668,7 @@ private BatchResponse getBatchResponse(String ...messageIds) { for (String messageId : messageIds) { listBuilder.add(SendResponse.fromMessageId(messageId)); } - return new BatchResponse(listBuilder.build()); + return new BatchResponseImpl(listBuilder.build()); } private static class MockFirebaseMessagingClient implements FirebaseMessagingClient { From ab3af3cda5937b4e335e0a9256e19cb76c9de4bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2020 15:36:27 -0700 Subject: [PATCH 108/456] Bump netty.version from 4.1.34.Final to 4.1.45.Final (#373) Bumps `netty.version` from 4.1.34.Final to 4.1.45.Final. Updates `netty-codec-http` from 4.1.34.Final to 4.1.45.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.1.34.Final...netty-4.1.45.Final) Updates `netty-handler` from 4.1.34.Final to 4.1.45.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.1.34.Final...netty-4.1.45.Final) Updates `netty-transport` from 4.1.34.Final to 4.1.45.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.1.34.Final...netty-4.1.45.Final) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index af25b87d7..b71b4e773 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.34.Final + 4.1.45.Final From f8052c76330364675b13b2724d6d951269ecd6e3 Mon Sep 17 00:00:00 2001 From: rsgowman Date: Tue, 12 May 2020 16:56:35 -0400 Subject: [PATCH 109/456] feat(auth): Add bulk get/delete methods (#365) This PR allows callers to retrieve a list of users by unique identifier (uid, email, phone, federated provider uid) as well as to delete a list of users. RELEASE NOTE: Added getUsers() and deleteUsers() APIs for retrieving and deleting user accounts in bulk. --- .../firebase/auth/DeleteUsersResult.java | 74 ++++++ .../google/firebase/auth/EmailIdentifier.java | 49 ++++ .../google/firebase/auth/FirebaseAuth.java | 143 ++++++++++- .../firebase/auth/FirebaseUserManager.java | 52 +++- .../google/firebase/auth/GetUsersResult.java | 52 ++++ .../google/firebase/auth/PhoneIdentifier.java | 49 ++++ .../firebase/auth/ProviderIdentifier.java | 56 +++++ .../google/firebase/auth/UidIdentifier.java | 49 ++++ .../google/firebase/auth/UserIdentifier.java | 31 +++ .../google/firebase/auth/UserMetadata.java | 15 +- .../com/google/firebase/auth/UserRecord.java | 16 +- .../auth/internal/BatchDeleteResponse.java | 51 ++++ .../auth/internal/GetAccountInfoRequest.java | 80 ++++++ .../auth/internal/GetAccountInfoResponse.java | 7 + .../google/firebase/auth/FirebaseAuthIT.java | 128 +++++++++- .../auth/FirebaseUserManagerTest.java | 227 ++++++++++++++++++ .../com/google/firebase/auth/GetUsersIT.java | 152 ++++++++++++ .../firebase/auth/ImportUserRecordTest.java | 2 +- 18 files changed, 1220 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/google/firebase/auth/DeleteUsersResult.java create mode 100644 src/main/java/com/google/firebase/auth/EmailIdentifier.java create mode 100644 src/main/java/com/google/firebase/auth/GetUsersResult.java create mode 100644 src/main/java/com/google/firebase/auth/PhoneIdentifier.java create mode 100644 src/main/java/com/google/firebase/auth/ProviderIdentifier.java create mode 100644 src/main/java/com/google/firebase/auth/UidIdentifier.java create mode 100644 src/main/java/com/google/firebase/auth/UserIdentifier.java create mode 100644 src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java create mode 100644 src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java create mode 100644 src/test/java/com/google/firebase/auth/GetUsersIT.java diff --git a/src/main/java/com/google/firebase/auth/DeleteUsersResult.java b/src/main/java/com/google/firebase/auth/DeleteUsersResult.java new file mode 100644 index 000000000..e8ca7dba5 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/DeleteUsersResult.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020 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.collect.ImmutableList; +import com.google.firebase.auth.internal.BatchDeleteResponse; +import com.google.firebase.internal.NonNull; +import java.util.List; + +/** + * Represents the result of the {@link FirebaseAuth#deleteUsersAsync(List)} API. + */ +public final class DeleteUsersResult { + + private final int successCount; + private final List errors; + + DeleteUsersResult(int users, BatchDeleteResponse response) { + ImmutableList.Builder errorsBuilder = ImmutableList.builder(); + List responseErrors = response.getErrors(); + if (responseErrors != null) { + checkArgument(users >= responseErrors.size()); + for (BatchDeleteResponse.ErrorInfo error : responseErrors) { + errorsBuilder.add(new ErrorInfo(error.getIndex(), error.getMessage())); + } + } + errors = errorsBuilder.build(); + successCount = users - errors.size(); + } + + /** + * Returns the number of users that were deleted successfully (possibly zero). Users that did not + * exist prior to calling {@link FirebaseAuth#deleteUsersAsync(List)} are considered to be + * successfully deleted. + */ + public int getSuccessCount() { + return successCount; + } + + /** + * Returns the number of users that failed to be deleted (possibly zero). + */ + public int getFailureCount() { + return errors.size(); + } + + /** + * A list of {@link ErrorInfo} instances describing the errors that were encountered during + * the deletion. Length of this list is equal to the return value of + * {@link #getFailureCount()}. + * + * @return A non-null list (possibly empty). + */ + @NonNull + public List getErrors() { + return errors; + } +} diff --git a/src/main/java/com/google/firebase/auth/EmailIdentifier.java b/src/main/java/com/google/firebase/auth/EmailIdentifier.java new file mode 100644 index 000000000..8e729c220 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/EmailIdentifier.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 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 com.google.firebase.auth.internal.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Used for looking up an account by email. + * + * @see {FirebaseAuth#getUsers} + */ +public final class EmailIdentifier extends UserIdentifier { + private final String email; + + public EmailIdentifier(@NonNull String email) { + UserRecord.checkEmail(email); + this.email = email; + } + + @Override + public String toString() { + return "EmailIdentifier(" + email + ")"; + } + + @Override + void populate(@NonNull GetAccountInfoRequest payload) { + payload.addEmail(email); + } + + @Override + boolean matches(@NonNull UserRecord userRecord) { + return email.equals(userRecord.getEmail()); + } +} diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index f7f6231ad..923778af4 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -42,8 +42,11 @@ import com.google.firebase.internal.Nullable; import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -601,7 +604,82 @@ protected UserRecord execute() throws FirebaseAuthException { } /** - * Gets a page of users starting from the specified {@code pageToken}. Page size will be + * Gets the user data corresponding to the specified identifiers. + * + *

      There are no ordering guarantees; in particular, the nth entry in the users result list is + * not guaranteed to correspond to the nth entry in the input parameters list. + * + *

      A maximum of 100 identifiers may be specified. If more than 100 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + * @param identifiers The identifiers used to indicate which user records should be returned. Must + * have 100 or fewer entries. + * @return The corresponding user records. + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 + * identifiers are specified. + * @throws NullPointerException If the identifiers parameter is null. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public GetUsersResult getUsers(@NonNull Collection identifiers) + throws FirebaseAuthException { + return getUsersOp(identifiers).call(); + } + + /** + * Gets the user data corresponding to the specified identifiers. + * + *

      There are no ordering guarantees; in particular, the nth entry in the users result list is + * not guaranteed to correspond to the nth entry in the input parameters list. + * + *

      A maximum of 100 identifiers may be specified. If more than 100 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + * @param identifiers The identifiers used to indicate which user records should be returned. + * Must have 100 or fewer entries. + * @return An {@code ApiFuture} that resolves to the corresponding user records. + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 + * identifiers are specified. + * @throws NullPointerException If the identifiers parameter is null. + */ + public ApiFuture getUsersAsync(@NonNull Collection identifiers) { + return getUsersOp(identifiers).callAsync(firebaseApp); + } + + private CallableOperation getUsersOp( + @NonNull final Collection identifiers) { + checkNotDestroyed(); + checkNotNull(identifiers, "identifiers must not be null"); + checkArgument(identifiers.size() <= FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE, + "identifiers parameter must have <= " + FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE + + " entries."); + + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected GetUsersResult execute() throws FirebaseAuthException { + Set users = userManager.getAccountInfo(identifiers); + Set notFound = new HashSet<>(); + for (UserIdentifier id : identifiers) { + if (!isUserFound(id, users)) { + notFound.add(id); + } + } + return new GetUsersResult(users, notFound); + } + }; + } + + private boolean isUserFound(UserIdentifier id, Collection userRecords) { + for (UserRecord userRecord : userRecords) { + if (id.matches(userRecord)) { + return true; + } + } + return false; + } + + /** + * Gets a page of users starting from the specified {@code pageToken}. Page size is * limited to 1000 users. * * @param pageToken A non-empty page token string, or null to retrieve the first page of users. @@ -842,8 +920,67 @@ protected Void execute() throws FirebaseAuthException { } /** - * Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a - * time. This operation is optimized for bulk imports and will ignore checks on identifier + * Deletes the users specified by the given identifiers. + * + *

      Deleting a non-existing user does not generate an error (the method is idempotent). + * Non-existing users are considered to be successfully deleted and are therefore included in the + * DeleteUsersResult.getSuccessCount() value. + * + *

      A maximum of 1000 identifiers may be supplied. If more than 1000 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + *

      This API has a rate limit of 1 QPS. Exceeding the limit may result in a quota exceeded + * error. If you want to delete more than 1000 users, we suggest adding a delay to ensure you + * don't exceed this limit. + * + * @param uids The uids of the users to be deleted. Must have <= 1000 entries. + * @return The total number of successful/failed deletions, as well as the array of errors that + * correspond to the failed deletions. + * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * identifiers are specified. + * @throws FirebaseAuthException If an error occurs while deleting users. + */ + public DeleteUsersResult deleteUsers(List uids) throws FirebaseAuthException { + return deleteUsersOp(uids).call(); + } + + /** + * Similar to {@link #deleteUsers(List)} but performs the operation asynchronously. + * + * @param uids The uids of the users to be deleted. Must have <= 1000 entries. + * @return An {@code ApiFuture} that resolves to the total number of successful/failed + * deletions, as well as the array of errors that correspond to the failed deletions. If an + * error occurs while deleting the user account, the future throws a + * {@link FirebaseAuthException}. + * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * identifiers are specified. + */ + public ApiFuture deleteUsersAsync(List uids) { + return deleteUsersOp(uids).callAsync(firebaseApp); + } + + private CallableOperation deleteUsersOp( + final List uids) { + checkNotDestroyed(); + checkNotNull(uids, "uids must not be null"); + for (String uid : uids) { + UserRecord.checkUid(uid); + } + checkArgument(uids.size() <= FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE, + "uids parameter must have <= " + FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE + + " entries."); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected DeleteUsersResult execute() throws FirebaseAuthException { + return userManager.deleteUsers(uids); + } + }; + } + + /** + * Imports the provided list of users into Firebase Auth. You can import a maximum of 1000 users + * at a time. This operation is optimized for bulk imports and does not check identifier * uniqueness which could result in duplications. * *

      {@link UserImportOptions} is required to import users with passwords. See diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 03c2813bc..ab8759c4f 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -39,9 +39,10 @@ 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.BatchDeleteResponse; import com.google.firebase.auth.internal.DownloadAccountResponse; +import com.google.firebase.auth.internal.GetAccountInfoRequest; import com.google.firebase.auth.internal.GetAccountInfoResponse; - import com.google.firebase.auth.internal.HttpErrorResponse; import com.google.firebase.auth.internal.UploadAccountResponse; import com.google.firebase.internal.ApiClientUtils; @@ -50,8 +51,11 @@ import com.google.firebase.internal.SdkUtils; import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * FirebaseUserManager provides methods for interacting with the Google Identity Toolkit via its @@ -86,6 +90,8 @@ class FirebaseUserManager { .put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain") .build(); + static final int MAX_GET_ACCOUNTS_BATCH_SIZE = 100; + static final int MAX_DELETE_ACCOUNTS_BATCH_SIZE = 1000; static final int MAX_LIST_USERS_RESULTS = 1000; static final int MAX_IMPORT_USERS = 1000; @@ -171,6 +177,33 @@ UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException return new UserRecord(response.getUsers().get(0), jsonFactory); } + Set getAccountInfo(@NonNull Collection identifiers) + throws FirebaseAuthException { + if (identifiers.isEmpty()) { + return new HashSet(); + } + + GetAccountInfoRequest payload = new GetAccountInfoRequest(); + for (UserIdentifier id : identifiers) { + id.populate(payload); + } + + GetAccountInfoResponse response = post( + "/accounts:lookup", payload, GetAccountInfoResponse.class); + + if (response == null) { + throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to parse server response"); + } + + Set results = new HashSet<>(); + if (response.getUsers() != null) { + for (GetAccountInfoResponse.User user : response.getUsers()) { + results.add(new UserRecord(user, jsonFactory)); + } + } + return results; + } + String createUser(CreateRequest request) throws FirebaseAuthException { GenericJson response = post( "/accounts", request.getProperties(), GenericJson.class); @@ -200,6 +233,23 @@ void deleteUser(String uid) throws FirebaseAuthException { } } + /** + * @pre uids != null + * @pre uids.size() <= MAX_DELETE_ACCOUNTS_BATCH_SIZE + */ + DeleteUsersResult deleteUsers(@NonNull List uids) throws FirebaseAuthException { + final Map payload = ImmutableMap.of( + "localIds", uids, + "force", true); + BatchDeleteResponse response = post( + "/accounts:batchDelete", payload, BatchDeleteResponse.class); + if (response == null) { + throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete users"); + } + + return new DeleteUsersResult(uids.size(), response); + } + DownloadAccountResponse listUsers(int maxResults, String pageToken) throws FirebaseAuthException { ImmutableMap.Builder builder = ImmutableMap.builder() .put("maxResults", maxResults); diff --git a/src/main/java/com/google/firebase/auth/GetUsersResult.java b/src/main/java/com/google/firebase/auth/GetUsersResult.java new file mode 100644 index 000000000..3ceec01cb --- /dev/null +++ b/src/main/java/com/google/firebase/auth/GetUsersResult.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020 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.checkNotNull; + +import com.google.firebase.internal.NonNull; +import java.util.Set; + +/** + * Represents the result of the {@link FirebaseAuth#getUsersAsync(Collection)} API. + */ +public final class GetUsersResult { + private final Set users; + private final Set notFound; + + GetUsersResult(@NonNull Set users, @NonNull Set notFound) { + this.users = checkNotNull(users); + this.notFound = checkNotNull(notFound); + } + + /** + * Set of user records corresponding to the set of users that were requested. Only users + * that were found are listed here. The result set is unordered. + */ + @NonNull + public Set getUsers() { + return this.users; + } + + /** + * Set of identifiers that were requested, but not found. + */ + @NonNull + public Set getNotFound() { + return this.notFound; + } +} diff --git a/src/main/java/com/google/firebase/auth/PhoneIdentifier.java b/src/main/java/com/google/firebase/auth/PhoneIdentifier.java new file mode 100644 index 000000000..bdc84fe92 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/PhoneIdentifier.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 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 com.google.firebase.auth.internal.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Used for looking up an account by phone number. + * + * @see {FirebaseAuth#getUsers} + */ +public final class PhoneIdentifier extends UserIdentifier { + private final String phoneNumber; + + public PhoneIdentifier(@NonNull String phoneNumber) { + UserRecord.checkPhoneNumber(phoneNumber); + this.phoneNumber = phoneNumber; + } + + @Override + public String toString() { + return "PhoneIdentifier(" + phoneNumber + ")"; + } + + @Override + void populate(@NonNull GetAccountInfoRequest payload) { + payload.addPhoneNumber(phoneNumber); + } + + @Override + boolean matches(@NonNull UserRecord userRecord) { + return phoneNumber.equals(userRecord.getPhoneNumber()); + } +} diff --git a/src/main/java/com/google/firebase/auth/ProviderIdentifier.java b/src/main/java/com/google/firebase/auth/ProviderIdentifier.java new file mode 100644 index 000000000..25e00026d --- /dev/null +++ b/src/main/java/com/google/firebase/auth/ProviderIdentifier.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 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 com.google.firebase.auth.internal.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Used for looking up an account by provider. + * + * @see {FirebaseAuth#getUsers} + */ +public final class ProviderIdentifier extends UserIdentifier { + private final String providerId; + private final String providerUid; + + public ProviderIdentifier(@NonNull String providerId, @NonNull String providerUid) { + UserRecord.checkProvider(providerId, providerUid); + this.providerId = providerId; + this.providerUid = providerUid; + } + + @Override + public String toString() { + return "ProviderIdentifier(" + providerId + ", " + providerUid + ")"; + } + + @Override + void populate(@NonNull GetAccountInfoRequest payload) { + payload.addFederatedUserId(providerId, providerUid); + } + + @Override + boolean matches(@NonNull UserRecord userRecord) { + for (UserInfo userInfo : userRecord.getProviderData()) { + if (providerId.equals(userInfo.getProviderId()) && providerUid.equals(userInfo.getUid())) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/google/firebase/auth/UidIdentifier.java b/src/main/java/com/google/firebase/auth/UidIdentifier.java new file mode 100644 index 000000000..a4f7069d9 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/UidIdentifier.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 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 com.google.firebase.auth.internal.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Used for looking up an account by uid. + * + * @see {FirebaseAuth#getUsers} + */ +public final class UidIdentifier extends UserIdentifier { + private final String uid; + + public UidIdentifier(@NonNull String uid) { + UserRecord.checkUid(uid); + this.uid = uid; + } + + @Override + public String toString() { + return "UidIdentifier(" + uid + ")"; + } + + @Override + void populate(@NonNull GetAccountInfoRequest payload) { + payload.addUid(uid); + } + + @Override + boolean matches(@NonNull UserRecord userRecord) { + return uid.equals(userRecord.getUid()); + } +} diff --git a/src/main/java/com/google/firebase/auth/UserIdentifier.java b/src/main/java/com/google/firebase/auth/UserIdentifier.java new file mode 100644 index 000000000..7ec9699e6 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/UserIdentifier.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 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 com.google.firebase.auth.internal.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Identifies a user to be looked up. + */ +public abstract class UserIdentifier { + public abstract String toString(); + + abstract void populate(@NonNull GetAccountInfoRequest payload); + + abstract boolean matches(@NonNull UserRecord userRecord); +} diff --git a/src/main/java/com/google/firebase/auth/UserMetadata.java b/src/main/java/com/google/firebase/auth/UserMetadata.java index a2872371f..85a24a0fd 100644 --- a/src/main/java/com/google/firebase/auth/UserMetadata.java +++ b/src/main/java/com/google/firebase/auth/UserMetadata.java @@ -23,14 +23,16 @@ public class UserMetadata { private final long creationTimestamp; private final long lastSignInTimestamp; + private final long lastRefreshTimestamp; public UserMetadata(long creationTimestamp) { - this(creationTimestamp, 0L); + this(creationTimestamp, 0L, 0L); } - public UserMetadata(long creationTimestamp, long lastSignInTimestamp) { + public UserMetadata(long creationTimestamp, long lastSignInTimestamp, long lastRefreshTimestamp) { this.creationTimestamp = creationTimestamp; this.lastSignInTimestamp = lastSignInTimestamp; + this.lastRefreshTimestamp = lastRefreshTimestamp; } /** @@ -50,4 +52,13 @@ public long getCreationTimestamp() { public long getLastSignInTimestamp() { return lastSignInTimestamp; } + + /** + * Returns the time at which the user was last active (ID token refreshed). + *  + * @return Milliseconds since epoch timestamp, or 0 if the user was never active. + */ + public long getLastRefreshTimestamp() { + return lastRefreshTimestamp; + } } diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index e00450079..64e7c278c 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.client.json.JsonFactory; +import com.google.api.client.util.DateTime; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -80,7 +81,15 @@ public class UserRecord implements UserInfo { } } this.tokensValidAfterTimestamp = response.getValidSince() * 1000; - this.userMetadata = new UserMetadata(response.getCreatedAt(), response.getLastLoginAt()); + + String lastRefreshAtRfc3339 = response.getLastRefreshAt(); + long lastRefreshAtMillis = 0; + if (!Strings.isNullOrEmpty(lastRefreshAtRfc3339)) { + lastRefreshAtMillis = DateTime.parseRfc3339(lastRefreshAtRfc3339).getValue(); + } + + this.userMetadata = new UserMetadata( + response.getCreatedAt(), response.getLastLoginAt(), lastRefreshAtMillis); this.customClaims = parseCustomClaims(response.getCustomClaims(), jsonFactory); } @@ -247,6 +256,11 @@ static void checkPhoneNumber(String phoneNumber) { "phone number must be a valid, E.164 compliant identifier starting with a '+' sign"); } + static void checkProvider(String providerId, String providerUid) { + checkArgument(!Strings.isNullOrEmpty(providerId), "providerId must be a non-empty string"); + checkArgument(!Strings.isNullOrEmpty(providerUid), "providerUid must be a non-empty string"); + } + static void checkUrl(String photoUrl) { checkArgument(!Strings.isNullOrEmpty(photoUrl), "url cannot be null or empty"); try { diff --git a/src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java b/src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java new file mode 100644 index 000000000..728cf6358 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020 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.api.client.util.Key; +import java.util.List; + +/** + * Represents the response from Google identity Toolkit for a batch delete request. + */ +public class BatchDeleteResponse { + + @Key("errors") + private List errors; + + public List getErrors() { + return errors; + } + + public static class ErrorInfo { + @Key("index") + private int index; + + @Key("message") + private String message; + + // A 'localId' field also exists here, but is not currently exposed in the Admin SDK. + + public int getIndex() { + return index; + } + + public String getMessage() { + return message; + } + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java new file mode 100644 index 000000000..67c4d0ee7 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020 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.api.client.util.Key; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the request to look up account information. + */ +public final class GetAccountInfoRequest { + + @Key("localId") + private List uids = null; + + @Key("email") + private List emails = null; + + @Key("phoneNumber") + private List phoneNumbers = null; + + @Key("federatedUserId") + private List federatedUserIds = null; + + private static final class FederatedUserId { + @Key("providerId") + private String providerId = null; + + @Key("rawId") + private String rawId = null; + + FederatedUserId(String providerId, String rawId) { + this.providerId = providerId; + this.rawId = rawId; + } + } + + public void addUid(String uid) { + if (uids == null) { + uids = new ArrayList<>(); + } + uids.add(uid); + } + + public void addEmail(String email) { + if (emails == null) { + emails = new ArrayList<>(); + } + emails.add(email); + } + + public void addPhoneNumber(String phoneNumber) { + if (phoneNumbers == null) { + phoneNumbers = new ArrayList<>(); + } + phoneNumbers.add(phoneNumber); + } + + public void addFederatedUserId(String providerId, String providerUid) { + if (federatedUserIds == null) { + federatedUserIds = new ArrayList<>(); + } + federatedUserIds.add(new FederatedUserId(providerId, providerUid)); + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java index 3d17c50f6..e84335891 100644 --- a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java +++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java @@ -73,6 +73,9 @@ public static class User { @Key("lastLoginAt") private long lastLoginAt; + @Key("lastRefreshAt") + private String lastRefreshAt; + @Key("validSince") private long validSince; @@ -119,6 +122,10 @@ public long getLastLoginAt() { return lastLoginAt; } + public String getLastRefreshAt() { + return lastRefreshAt; + } + public long getValidSince() { return validSince; } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 6a8361d0d..aa45f51b3 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -137,6 +137,78 @@ public void testDeleteNonExistingUser() throws Exception { } } + @Test + public void testDeleteUsers() throws Exception { + UserRecord user1 = newUserWithParams(); + UserRecord user2 = newUserWithParams(); + UserRecord user3 = newUserWithParams(); + + DeleteUsersResult deleteUsersResult = + slowDeleteUsersAsync(ImmutableList.of(user1.getUid(), user2.getUid(), user3.getUid())) + .get(); + + assertEquals(3, deleteUsersResult.getSuccessCount()); + assertEquals(0, deleteUsersResult.getFailureCount()); + assertTrue(deleteUsersResult.getErrors().isEmpty()); + + GetUsersResult getUsersResult = + auth.getUsersAsync( + ImmutableList.of(new UidIdentifier(user1.getUid()), + new UidIdentifier(user2.getUid()), new UidIdentifier(user3.getUid()))) + .get(); + + assertTrue(getUsersResult.getUsers().isEmpty()); + assertEquals(3, getUsersResult.getNotFound().size()); + } + + @Test + public void testDeleteExistingAndNonExistingUsers() throws Exception { + UserRecord user1 = newUserWithParams(); + + DeleteUsersResult deleteUsersResult = + slowDeleteUsersAsync(ImmutableList.of(user1.getUid(), "uid-that-doesnt-exist")).get(); + + assertEquals(2, deleteUsersResult.getSuccessCount()); + assertEquals(0, deleteUsersResult.getFailureCount()); + assertTrue(deleteUsersResult.getErrors().isEmpty()); + + GetUsersResult getUsersResult = + auth.getUsersAsync(ImmutableList.of(new UidIdentifier(user1.getUid()), + new UidIdentifier("uid-that-doesnt-exist"))) + .get(); + + assertTrue(getUsersResult.getUsers().isEmpty()); + assertEquals(2, getUsersResult.getNotFound().size()); + } + + @Test + public void testDeleteUsersIsIdempotent() throws Exception { + UserRecord user1 = newUserWithParams(); + + DeleteUsersResult result = slowDeleteUsersAsync(ImmutableList.of(user1.getUid())).get(); + + assertEquals(1, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + assertTrue(result.getErrors().isEmpty()); + + // Delete the user again to ensure that everything still counts as a success. + result = slowDeleteUsersAsync(ImmutableList.of(user1.getUid())).get(); + + assertEquals(1, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + assertTrue(result.getErrors().isEmpty()); + } + + /** + * The {@code batchDelete} endpoint has a rate limit of 1 QPS. Use this test + * helper to ensure you don't exceed the quota. + */ + // TODO(rsgowman): When/if the rate limit is relaxed, eliminate this helper. + private ApiFuture slowDeleteUsersAsync(List uids) throws Exception { + TimeUnit.SECONDS.sleep(1); + return auth.deleteUsersAsync(uids); + } + @Test public void testCreateUserWithParams() throws Exception { RandomUser randomUser = RandomUser.create(); @@ -248,6 +320,35 @@ public void testUserLifecycle() throws Exception { } } + @Test + public void testLastRefreshTime() throws Exception { + RandomUser user = RandomUser.create(); + UserRecord newUserRecord = auth.createUser(new CreateRequest() + .setUid(user.uid) + .setEmail(user.email) + .setEmailVerified(false) + .setPassword("password")); + + try { + // New users should not have a lastRefreshTimestamp set. + assertEquals(0, newUserRecord.getUserMetadata().getLastRefreshTimestamp()); + + // Login to cause the lastRefreshTimestamp to be set. + signInWithPassword(newUserRecord.getEmail(), "password"); + + UserRecord userRecord = auth.getUser(newUserRecord.getUid()); + + // Ensure the lastRefreshTimestamp is approximately "now" (with a tollerance of 10 minutes). + long now = System.currentTimeMillis(); + long tollerance = TimeUnit.MINUTES.toMillis(10); + long lastRefreshTimestamp = userRecord.getUserMetadata().getLastRefreshTimestamp(); + assertTrue(now - tollerance <= lastRefreshTimestamp); + assertTrue(lastRefreshTimestamp <= now + tollerance); + } finally { + auth.deleteUser(newUserRecord.getUid()); + } + } + @Test public void testListUsers() throws Exception { final List uids = new ArrayList<>(); @@ -607,7 +708,7 @@ private Map parseLinkParameters(String link) throws Exception { return result; } - private String randomPhoneNumber() { + static String randomPhoneNumber() { Random random = new Random(); StringBuilder builder = new StringBuilder("+1"); for (int i = 0; i < 10; i++) { @@ -637,7 +738,7 @@ private String signInWithPassword(String email, String password) throws IOExcept GenericUrl url = new GenericUrl(VERIFY_PASSWORD_URL + "?key=" + IntegrationTestUtils.getApiKey()); Map content = ImmutableMap.of( - "email", email, "password", password); + "email", email, "password", password, "returnSecureToken", true); HttpRequest request = transport.createRequestFactory().buildPostRequest(url, new JsonHttpContent(jsonFactory, content)); request.setParser(new JsonObjectParser(jsonFactory)); @@ -696,9 +797,9 @@ private void checkRecreate(String uid) throws Exception { } } - private static class RandomUser { - private final String uid; - private final String email; + static class RandomUser { + final String uid; + final String email; private RandomUser(String uid, String email) { this.uid = uid; @@ -712,4 +813,21 @@ static RandomUser create() { return new RandomUser(uid, email); } } + + static UserRecord newUserWithParams() throws Exception { + return newUserWithParams(auth); + } + + static UserRecord newUserWithParams(FirebaseAuth auth) throws Exception { + // TODO(rsgowman): This function could be used throughout this file (similar to the other + // ports). + RandomUser randomUser = RandomUser.create(); + return auth.createUser(new CreateRequest() + .setUid(randomUser.uid) + .setEmail(randomUser.email) + .setPhoneNumber(randomPhoneNumber()) + .setDisplayName("Random User") + .setPhotoUrl("https://example.com/photo.png") + .setPassword("password")); + } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index de0b7fa29..67d0448e9 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -33,6 +33,7 @@ import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -41,6 +42,8 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; +import com.google.firebase.auth.UidIdentifier; +import com.google.firebase.auth.UserIdentifier; import com.google.firebase.auth.UserRecord.CreateRequest; import com.google.firebase.auth.UserRecord.UpdateRequest; import com.google.firebase.internal.SdkUtils; @@ -52,6 +55,8 @@ import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; @@ -167,6 +172,153 @@ public void testGetUserByPhoneNumberWithNotFoundError() throws Exception { } } + @Test + public void testGetUsersExceeds100() throws Exception { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + List identifiers = new ArrayList<>(); + for (int i = 0; i < 101; i++) { + identifiers.add(new UidIdentifier("uid_" + i)); + } + + try { + FirebaseAuth.getInstance().getUsers(identifiers); + fail("No error thrown for too many supplied identifiers"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testGetUsersNull() throws Exception { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + try { + FirebaseAuth.getInstance().getUsers(null); + fail("No error thrown for null identifiers"); + } catch (NullPointerException expected) { + // expected + } + } + + @Test + public void testGetUsersEmpty() throws Exception { + initializeAppForUserManagement(); + GetUsersResult result = FirebaseAuth.getInstance().getUsers(new ArrayList()); + assertTrue(result.getUsers().isEmpty()); + assertTrue(result.getNotFound().isEmpty()); + } + + @Test + public void testGetUsersAllNonExisting() throws Exception { + initializeAppForUserManagement("{ \"users\": [] }"); + List ids = ImmutableList.of( + new UidIdentifier("id-that-doesnt-exist")); + GetUsersResult result = FirebaseAuth.getInstance().getUsers(ids); + assertTrue(result.getUsers().isEmpty()); + assertEquals(ids.size(), result.getNotFound().size()); + assertTrue(result.getNotFound().containsAll(ids)); + } + + @Test + public void testGetUsersMultipleIdentifierTypes() throws Exception { + initializeAppForUserManagement(("" + + "{ " + + " 'users': [{ " + + " 'localId': 'uid1', " + + " 'email': 'user1@example.com', " + + " 'phoneNumber': '+15555550001' " + + " }, { " + + " 'localId': 'uid2', " + + " 'email': 'user2@example.com', " + + " 'phoneNumber': '+15555550002' " + + " }, { " + + " 'localId': 'uid3', " + + " 'email': 'user3@example.com', " + + " 'phoneNumber': '+15555550003' " + + " }, { " + + " 'localId': 'uid4', " + + " 'email': 'user4@example.com', " + + " 'phoneNumber': '+15555550004', " + + " 'providerUserInfo': [{ " + + " 'providerId': 'google.com', " + + " 'rawId': 'google_uid4' " + + " }] " + + " }] " + + "} " + ).replace("'", "\"")); + + UidIdentifier doesntExist = new UidIdentifier("this-uid-doesnt-exist"); + List ids = ImmutableList.of( + new UidIdentifier("uid1"), + new EmailIdentifier("user2@example.com"), + new PhoneIdentifier("+15555550003"), + new ProviderIdentifier("google.com", "google_uid4"), + doesntExist); + GetUsersResult result = FirebaseAuth.getInstance().getUsers(ids); + Collection uids = userRecordsToUids(result.getUsers()); + assertTrue(uids.containsAll(ImmutableList.of("uid1", "uid2", "uid3", "uid4"))); + assertEquals(1, result.getNotFound().size()); + assertTrue(result.getNotFound().contains(doesntExist)); + } + + private Collection userRecordsToUids(Collection userRecords) { + Collection uids = new HashSet<>(); + for (UserRecord userRecord : userRecords) { + uids.add(userRecord.getUid()); + } + return uids; + } + + @Test + public void testInvalidUidIdentifier() throws Exception { + try { + new UidIdentifier("too long " + Strings.repeat(".", 128)); + fail("No error thrown for invalid uid"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidEmailIdentifier() throws Exception { + try { + new EmailIdentifier("invalid email addr"); + fail("No error thrown for invalid email"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidPhoneIdentifier() throws Exception { + try { + new PhoneIdentifier("invalid phone number"); + fail("No error thrown for invalid phone number"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidProviderIdentifier() throws Exception { + try { + new ProviderIdentifier("", "valid-uid"); + fail("No error thrown for invalid provider id"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ProviderIdentifier("valid-id", ""); + fail("No error thrown for invalid provider uid"); + } catch (IllegalArgumentException expected) { + // expected + } + } + @Test public void testListUsers() throws Exception { final TestResponseInterceptor interceptor = initializeAppForUserManagement( @@ -278,6 +430,81 @@ public void testDeleteUser() throws Exception { checkRequestHeaders(interceptor); } + @Test + public void testDeleteUsersExceeds1000() throws Exception { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + List ids = new ArrayList<>(); + for (int i = 0; i < 1001; i++) { + ids.add("id" + i); + } + try { + FirebaseAuth.getInstance().deleteUsersAsync(ids); + fail("No error thrown for too many uids"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testDeleteUsersInvalidId() throws Exception { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + try { + FirebaseAuth.getInstance().deleteUsersAsync( + ImmutableList.of("too long " + Strings.repeat(".", 128))); + fail("No error thrown for too long uid"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testDeleteUsersIndexesErrorsCorrectly() throws Exception { + initializeAppForUserManagement(("" + + "{ " + + " 'errors': [{ " + + " 'index': 0, " + + " 'localId': 'uid1', " + + " 'message': 'NOT_DISABLED : Disable the account before batch deletion.' " + + " }, { " + + " 'index': 2, " + + " 'localId': 'uid3', " + + " 'message': 'something awful' " + + " }] " + + "} " + ).replace("'", "\"")); + + DeleteUsersResult result = FirebaseAuth.getInstance().deleteUsersAsync(ImmutableList.of( + "uid1", "uid2", "uid3", "uid4" + )).get(); + + assertEquals(2, result.getSuccessCount()); + assertEquals(2, result.getFailureCount()); + assertEquals(2, result.getErrors().size()); + assertEquals(0, result.getErrors().get(0).getIndex()); + assertEquals( + "NOT_DISABLED : Disable the account before batch deletion.", + result.getErrors().get(0).getReason()); + assertEquals(2, result.getErrors().get(1).getIndex()); + assertEquals("something awful", result.getErrors().get(1).getReason()); + } + + @Test + public void testDeleteUsersSuccess() throws Exception { + initializeAppForUserManagement("{}"); + + DeleteUsersResult result = FirebaseAuth.getInstance().deleteUsersAsync(ImmutableList.of( + "uid1", "uid2", "uid3" + )).get(); + + assertEquals(3, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + assertTrue(result.getErrors().isEmpty()); + } + @Test public void testImportUsers() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); diff --git a/src/test/java/com/google/firebase/auth/GetUsersIT.java b/src/test/java/com/google/firebase/auth/GetUsersIT.java new file mode 100644 index 000000000..efe2f783f --- /dev/null +++ b/src/test/java/com/google/firebase/auth/GetUsersIT.java @@ -0,0 +1,152 @@ +/* + * Copyright 2020 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.assertTrue; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.FirebaseApp; +import com.google.firebase.testing.IntegrationTestUtils; +import java.util.Collection; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class GetUsersIT { + private static FirebaseAuth auth; + private static UserRecord testUser1; + private static UserRecord testUser2; + private static UserRecord testUser3; + private static String importUserUid; + + @BeforeClass + public static void setUpClass() throws Exception { + FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp(); + auth = FirebaseAuth.getInstance(masterApp); + + testUser1 = FirebaseAuthIT.newUserWithParams(auth); + testUser2 = FirebaseAuthIT.newUserWithParams(auth); + testUser3 = FirebaseAuthIT.newUserWithParams(auth); + + FirebaseAuthIT.RandomUser randomUser = FirebaseAuthIT.RandomUser.create(); + importUserUid = randomUser.uid; + String phone = FirebaseAuthIT.randomPhoneNumber(); + UserImportResult result = auth.importUsers(ImmutableList.of( + ImportUserRecord.builder() + .setUid(randomUser.uid) + .setEmail(randomUser.email) + .setPhoneNumber(phone) + .addUserProvider( + UserProvider.builder() + .setProviderId("google.com") + .setUid("google_" + randomUser.uid) + .build()) + .build() + )); + assertEquals(1, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + } + + @AfterClass + public static void cleanup() throws Exception { + // TODO(rsgowman): deleteUsers (plural) would make more sense here, but it's currently rate + // limited to 1qps. When/if that's relaxed, change this to just delete them all at once. + auth.deleteUser(testUser1.getUid()); + auth.deleteUser(testUser2.getUid()); + auth.deleteUser(testUser3.getUid()); + auth.deleteUser(importUserUid); + } + + @Test + public void testVariousIdentifiers() throws Exception { + GetUsersResult result = auth.getUsersAsync(ImmutableList.of( + new UidIdentifier(testUser1.getUid()), + new EmailIdentifier(testUser2.getEmail()), + new PhoneIdentifier(testUser3.getPhoneNumber()), + new ProviderIdentifier("google.com", "google_" + importUserUid) + )).get(); + + Collection expectedUids = ImmutableList.of( + testUser1.getUid(), testUser2.getUid(), testUser3.getUid(), importUserUid); + + assertTrue(sameUsers(result.getUsers(), expectedUids)); + assertEquals(0, result.getNotFound().size()); + } + + @Test + public void testIgnoresNonExistingUsers() throws Exception { + UidIdentifier doesntExistId = new UidIdentifier("uid_that_doesnt_exist"); + GetUsersResult result = auth.getUsersAsync(ImmutableList.of( + new UidIdentifier(testUser1.getUid()), + doesntExistId, + new UidIdentifier(testUser3.getUid()) + )).get(); + + Collection expectedUids = ImmutableList.of(testUser1.getUid(), testUser3.getUid()); + + assertTrue(sameUsers(result.getUsers(), expectedUids)); + assertEquals(1, result.getNotFound().size()); + assertTrue(result.getNotFound().contains(doesntExistId)); + } + + @Test + public void testOnlyNonExistingUsers() throws Exception { + UidIdentifier doesntExistId = new UidIdentifier("uid_that_doesnt_exist"); + GetUsersResult result = auth.getUsersAsync(ImmutableList.of( + doesntExistId + )).get(); + + assertEquals(0, result.getUsers().size()); + assertEquals(1, result.getNotFound().size()); + assertTrue(result.getNotFound().contains(doesntExistId)); + } + + @Test + public void testDedupsDuplicateUsers() throws Exception { + GetUsersResult result = auth.getUsersAsync(ImmutableList.of( + new UidIdentifier(testUser1.getUid()), + new UidIdentifier(testUser1.getUid()) + )).get(); + + Collection expectedUids = ImmutableList.of(testUser1.getUid()); + + assertEquals(1, result.getUsers().size()); + assertTrue(sameUsers(result.getUsers(), expectedUids)); + assertEquals(0, result.getNotFound().size()); + } + + /** + * Checks to see if the userRecords collection contains the given uids. + * + *

      Behaviour is undefined if there are duplicate entries in either of the parameters. + */ + private boolean sameUsers(Collection userRecords, Collection uids) { + if (userRecords.size() != uids.size()) { + return false; + } + + for (UserRecord userRecord : userRecords) { + if (!uids.contains(userRecord.getUid())) { + return false; + } + } + + return true; + } +} diff --git a/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java b/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java index 011a5cc04..e2ae36c09 100644 --- a/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java +++ b/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java @@ -62,7 +62,7 @@ public void testAllProperties() throws IOException { .setDisplayName("Test User") .setPhotoUrl("https://test.com/user.png") .setPhoneNumber("+1234567890") - .setUserMetadata(new UserMetadata(date.getTime(), date.getTime())) + .setUserMetadata(new UserMetadata(date.getTime(), date.getTime(), date.getTime())) .setDisabled(false) .setEmailVerified(true) .setPasswordHash("password".getBytes()) From 30801e312b53215d60c8c9a79fa9f3b7c9cd1d00 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 13 May 2020 10:37:37 -0700 Subject: [PATCH 110/456] chore: Setting the version of the Maven Javadoc plugin (#412) --- pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pom.xml b/pom.xml index b71b4e773..d89d227e1 100644 --- a/pom.xml +++ b/pom.xml @@ -99,6 +99,7 @@ maven-javadoc-plugin + 2.10.4 site @@ -303,6 +304,7 @@ maven-javadoc-plugin + 2.10.4 attach-javadocs From a4a5315f621ef48e026ab9058590b7977a6d8873 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 14 May 2020 11:01:54 -0700 Subject: [PATCH 111/456] [chore] Release 6.13.0 (#413) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d89d227e1..ca1657fb8 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.12.3-SNAPSHOT + 6.13.0 jar firebase-admin From 200d1f9efebb03201026d7f3b92857b954099676 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 14 May 2020 12:05:15 -0700 Subject: [PATCH 112/456] [chore] Release 6.13.0 take 2 (#414) * Upgated the gpg keys * Added temp verify script * Disabled tty for gpg import * Removing temp verification script * Updated publish commands --- .github/resources/firebase.asc.gpg | Bin 4056 -> 4061 bytes .github/scripts/publish_artifacts.sh | 2 +- .github/workflows/release.yml | 16 ++++++++-------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/resources/firebase.asc.gpg b/.github/resources/firebase.asc.gpg index a946776c7fd1498a0399836a94785dc8fd5293ed..8fd7d0769b6056896ebff240b9955656f7c98a60 100644 GIT binary patch literal 4061 zcmV<34>TRDh12twaxxgrB=~nu4Ie_l{B4=u%Z!ZxI0Yr=F9w=iT{b&g_ zw-c$@r|V{b{y-ay=@PtDT7)9C*C09hepY{5sNuZs*%)_x^`u}LsM4fZ+W>w_1huO( z%@c>7hIE>+#}dDaru{9lKFrrwJ~m)gys#I`B@&VNwDtH@F5_E$O!~J06yi;j2f+Lr=e)3IkT*mj|4P^3n6MwY;}sTk0@P%l&sf zVK}afzDH%-Ga~R6e&tt7&eFqLx~Kt9?Ch$wFpKravh*+vc|l1uZmRT zQg6pu(L&zpsvS1Fb0qpUAjJEAsZi&$WXyKuG&`;?AOE@od`tm&azIt-fvFjs$A77A zfwiwhOi#eI35UZx0i*oi8-BQlmhFg1W38YzFUkDZSkQT$cL`$mecv+p`NPne#*Q-` z^a}Xh-T%r6-fU1CT|MxK0}z0VwojB5_C35grZ}bVI<37u$q9V-snzMXRL`QH_s;pd z+JH9TQ3Tw1_n<7(glRHMhS2AYVoU$#GSlj$XkS710%aSznI4joqCgvZ8J;d2{xi-G zK#|Pk6-9oXY5<5P6d%XxOaui|Bvt2T5dgXZ-Z%fG=6C|0j14qIKw&}K-pT;f7#{&9 zrBvU-Hdib)$r$$lx#w4fIXS;V_9N4S`0CHoS!gc*q<#Weu{g2yOR0_ zLbV|rO2){j48X0>)z~!Wk735N1OfNmiePSpB~vNurH6r%&9R^AwdTu!-=RB@6$tOu zFD=1luY?>%-(4`F8+^R6DLU_c{Nf$L_S8=~nt^V~jg>_`a?(rdv4M}co;tcE84tR_ zbAFf;!T+q!6?VThSH2VesUu>vHl-E^v@#LXC~kLnjP}6%{CCYDQ(3wh5V?QDqf2px zUmyq|`daYnw_i~B8m^)fz5@b!?m`tNh|z^!xxV8Xw6G+gqxC}?hW67f{z~FZq59W6 z^^@;&xL)PZHT$aB%p%!D9HP&qJscOSi=^~T{BaEaVnLFx(e3PN$e%KKcYSXCR11LpRq)QJWnyvh&58kSVK<%e=hI% zdOEP>StfE5yeD99e2B+Q=BP+~X2_;q4*+0!vY}E5Iq4$uVb&`1b)u6Ib4Z>3DYBbD zydh-S5Km;^20iFd-8#w>=r_PDxMpR5Z)Sv}fjB8ED2G=fD)CevCX&$?%kMrRGRQKr zlhjT$1oB1@bC+L!g5=3!P#@>zWm#0DBHi3d>V)*~&=xo02z@S9xX$~fLIgt337n=y zWC&~j_SM?|$JU{?Hj-B4eYqcEE6HK`ewdgRj{E``^)!rAcn&Y(E(Su6x2*k5QqFkdLqoSylqxE(D&K1sj+ zq{)|j&VLF{*+I${cFiX^m6e|Mg>OOhKdY3Ug+;t{+hO9BqxN+zT`;vAaz9gKi5&t;6N9hQC8Jh~Lb&mxdoDzRfgu=JkqV@QlhVO z#DG|EbK$Bl5m4~9n;t{`A=*xPn+VP{ZOPTF%Rix5qI?tm5~H)4L(9AOa*FcC#!4h$ zay6GGE+!(A@34Nh-_9@+-IAn}t^q~`fp6Ypgk?7&5IM6cdlo=oo<2Y)evnV$5N0Q9 z(d^pc&3h9aPo{Ri`3mGV-(Z*yre1c7xmhKmCIWF2fAy~B62OYXzIh7q)+rjmx2d%r zG&BL~G9NN%Wdu;P6A-}l7mB3#;};>bLJu%Zf5*LD!SQxit_ydd%RCSOG)5T2ZN++v znv=;S8`vSB0@e|sS#z#20S`Z-L?o`0z3Tj})7$;JX=*RR)KqnjZ9!=mpbJt8hpsRR zS98jB&cJm}8r>@$rK}rb`>AihF?l#SJOC>LChFmA3>=2detiMok)NLr>PR|A>_hZ+ zIx>&#L=O4sapFtit zwi+1b%q~hcr{1P-_W3FUYfdlArUU_HKeOSBakbl zGsPs&JSxcOA7$L&B!TI@HwL}Ed9#lEQ4Ph+M3!tREoCx>p|;=tr`)5uLr1<4!4)$M#KvhVNWK&n4m4$pu};hZKI;2? zRD}0kq_+arS4lQ0J zyV!~G@};dT53c&SFB-gJ%KFJyvgKTO?69z~7}SrHXCB-|pU>@Jx71J3ZEv_<5Q8q) zkt`{Wh#aZ~1)7|o*dGQktcJ+0_NYL3UxK6pUUWVikKFc-=fzqrniwP!K7%dNV`Rvm zwI5o@g$isY*jX&K{GZ?i=+SmL_Vul(ROv#oMj{eqRioz26fx7Tpdov3mdKzW)}r0r z2!CL#4s6j<&CH~ls&lZaPn7>%%o}NlnG%5DO;d#Dwt&;6NvEy>sN=`7db^O^)i)$~ z4@&yo4XW4S|t{h`jM_)%krF4g_bJcZ6>dk`_WNZ4nHcF@2(5IuCgy$!3U2@`Z2TnKK~3Xl0JTzVNg}$Z(RpH)to$6lf|FFF zGS{o&YE@nLc^U^}fNxAWgl|JrZIY|$^(|wjvFS$3Cf0W)kFuCTQrCDLnnWY=8a7Q6 zQM`YT4>_ktr+<{7=q5A^xn=4^tSmyIs;)BcW9>{eoHtbTN*p%9WltA)oxjT&D3Mb-E!8h6)>= ztCx0Ml9{PAY|aDh>Ldw^9al>|o?eW><8J`aY0>~&&=b=Q0(KrCb{}LpX!bg7v_HDx z`KM|v-ie?zh?24vxs2;fPVS`gVas!_;C29sG6Osdq%p?@nZ!ON-vpZz-g z>58&!*nw1zH-!A5`bI2^`O_r)zq#;3@xFVy#*S+OlTR@21g0ARQSD&UkE1uzyd8E3 zgLcJ9*X57fRX4uTJni0buamcXX37vnR&BL8zFMO}Lgc9qO`c)oY5=%a$4*RKMQ3F? zSRs(o+f_N`CSh2lO5sA9kHbsK*Yr%>yFADTS^~|}A*AATR6O^!#Kvd!cbY963!(|l z^nkJ+FJe?ski7dpkYt)sNZzA#RKqd0rr{vfoQYI}1U|pGDt^;=g;sC4L$8gUwrT3; zEUDvmKIN@72!otR4vr0dMaHq-)oQ}~a{lHfV#nKZ+-QC7>!iWG;-OA}N3XFq=`RG| z$LQHwMAu@sDw}|UE-b*9ZJS0^SCdD0_Qlkf_%uAQ(U{diEIPV4@j z<<`PYX%@`#0!a0tdWO*|3C1FrKnAiL!1SerD<}nDCQ z2so>P8Xz5P`%krULZ%hwWtW!=gEFCn@5ZoJkLvVsjQj?_gTijiyjQd;s;+?fEMEIl zCW_+-tHe1(pA!HtoTeq|lxrM{iU@)90oap)GEIqUB-CIKkMVwgc7A5iMm|VXlX9c;YSx z3RIqP!LqF6ofr8ell_En&(EMvLLs1qA4|f=kV=jbPQ76z_u0iIsBI((a|_S(VY-`*o&I+#7og2mvd1bVY^0 zA*7F*e;zlI^~iUzzhHPPLG^UG8LnIYHnC=D_HwMyZ8+%J>EEXHy*$CCin!zIgn)r& zeHOTG)WpHuyy3PDf7ogl1~I`lrUe)%qVtb$eDdDeZ@A8iku6Tjet@7P)bfU7#guou zNb?bve9@1LDToX%)Ev253ot{ZD&&1G#`Y`jLmP#YO>Q=2tLod!NV>DwuooOnEEYwd z%wu*~D`sKW5)K2S;}>U+y_=@mAnxRQ6opOoWzm#mRSppcyD!tUjZM|;Px{@dP zBG{0!Sh6P!W{N>7`Iv7y{~m4XVvzfBP=^b<7s5>mK*)n!RHgfXGw_f31aJ&4N^sd} zKi}q$0l3trF9dJx;9_Px#Vj?A_#0mF_z0|j*R$(DO7Xr zZpNqdnM~+ZY>e@$;S(kIV-V^a@J8BL)H&)4@&9HzfXT=Il+j@CSI@?zoDiESCgN>^ z#pHteO6U&MCvWmwSq8|;RRUvm<3^0BE|>K0i~@WuuaYpOz&I*9xh8}|@dg%V&CF|L zjFb)OOOe8-KaVW=c#S`Ss}RlrOZ?#O*e$Zfq;e%ryTPGShAnu~lCvuHZMrihx7xlx zOldQSRd0@j9j--aCzT7^QKEl=V$5g~I@@Wwp>jI3>!f7QuG#g>wSdkhT2p?%dzVVg5laj~V|q>A|JFUL}b98ib12!uE-b?Bau8(pUW}a91l>SVGGx;XOV)6025jq&C z=twVclr4(}0I?N&^aaVxtCBQCR))pC@EqriFb%+-DI5CrhP_PhAa9Y7I)@w zAM_59o0Qvqd3}Z+V6=o`M>WWVXE+&!?nF zLvnv8yA!_8okUw9?3-ONUs2IYTt}$V2|G^fgO<7OMVu!PXI9%Et z5s1`~8iy6(0=AAfa8|uITcKp490(SSVaum>4=Jh>ronEnW>OrY9wJ!mAe9Nte2*on?U zT?a*nuNb5(!8XBRAX&Flbv`2-6JPU)+QZd~>Y47db7Jx#eK<&zabmE98=cxKS(4eH z`~LxwSvU7Ve(gS#jSZH>(QfE86u7?#JTl6+$H~qAfD&Azhw=VtmZGh1f}l5yRriKO zX3M|cmHms6?b7brMTiPh)v#(~tj$p{+VI59Ahkf0I%tVP+Xj?1R=dnULjCiSmB;Cc zzX@;6nt{_4HjG2s=ocb|Z(~geFdCvH!qGK3{H6$Tt;%08lj+{Xb`OMw=DGkjlV{h_ zx9iiwLi3&5l>!iJ$*7<_?7dQnf`W4u`o2_78XQrmKSF^Z!L(q$2Cx*)8^bN}x$4h2 zPc!gA|Ee6=3VN*$WfOlPn2oiNcVVv4l}{Lag$6_g#i{+wc3b{JPE})$0Ls`Ly8NKx zaLzE8Ox77z1_sYV%zeTx%^~OcbNhtxQXGc!?+7{iV6Y1^7Gz;g&;d;w>0lgUe22E~ zroyyWSlW?m69Sv_H8w6(} z+xj5L1e3|mf@0}hxL)4wM&~%rV~a8p(g*_onDrC7%H3ckPbodKsE&y}hUW^iPB0YS zKQVl&_8p>?Iz|#3+QB^u4(nN<>I+5Os+4wTYOmAEP0i5wS%O$RFCHYuBN(C%R6Hs; zOe)l~$Ku1MN0fcI8&0cra9jplsOyUl3?+lOylU)YyUv8f2az|~#pXg{P<%~TSPV&% zyM^15lsNqI$ClcwXIjr;vmG5Alzg23T>^XwXFP=lXA|xipmA^L=c_Yb5tug$9cuS$ z25^~Q8azMB>oq7o#U-|4SIq)>HQp2Iy!uc>s#*_X71mG*Qm32k%8h<9w0}0LhcGd3 zxYyQGK+!}J1_V$Y>EobOVhcOY!xcBR5CA}j2WC-@<=w9j)-`uUaj#DLF!_HznM#Lz zY%1K9k61k8JS!~W!OCJ@!r+fds8CMcc^4VVHOp*umI1e0un{e=lkW@3-8A6{H$63bSEi^yFI;=q3{WtI|mI_}hmX5=8hD3!XCA4jA)ty$VeRhT!cfcTUV+ zK3XpIA(E-?V%<@jAurSB3lw2Ip%BO>p|Tu2=1Eez7S{=0wQmt8Yv{KmyNz(#8^qlq zW7rZ}3i8jb5eukdrQv0!PQOcOhev=~y-T+%1Mv+VJr2tJ7Mu$PMenudb8iF(prngJ zIEuBG;~hkP?>IDcTO@3vUC#;HCv$Sv!VdM#5&0>P-v&(K2e2Hzx{K*a>fXmcR1wqe3Z8{5+CFGDx5l!PvCe#wMNn%eApGB0;`?EY{JZHY`28~L zB=22wfOTv01{hJNXn8(G!PWNTuNq|dB)6xI$2^`NHro7vTmPmbFM<^KefX_TSPTg; zl*h&qo-dZ+$%a5yUNi+tbm(|zu={)Do3<1A4CNiurj6(It`Frh*sGKl=1UQN^^E^I z9FAjNimgI(YM*F+B6aANlQ&8Z9gXhok+j^{6zSo(cN8!i{KS9X+s{!r#x^7@kbth) z$+b5ul8vGL{J!_#OB^uHd&4K3dBk7rB~J$)fo;OFm%@aVAGE(ZPCZ1PrH(KtMo1~t z+Y9X5?o;j~Z5MN=C*A$7-a(UHc|fPyuFh^e!{~wJAJ9AzO;jj^hX#tL2Y@Me2++(m z){lC!c;^egc?B^a)Fla4>M3YTZ#5b=&OciVJCQDF<h~nMeWb9n*u6nmnT!UW{+K-Fpba>kTBsChG!vmLMk7CNGIo3=d-`YOBWZ@wT1{K^ zD0*+k1^Uy=_fL1&o()tA>K3E944{K;J8f6c=M9sXM?|H>?rUP&o0@7=QYB5|+Q{s^ zu4p?+s8d$@zcO6->L*;nGJXL_Y3Sv^_yF z=G;x4p%gDB0zUvhxk#G?iGD8Yk5dy6yGI5VlHX&d@l4ebjr;x8?iHtp&U%4@ZLjf~ zt%J+l3NK6v&&7VEwTV&Ely+pvh~5-Cs&qAo&5S*YA8?Jj?X}*PKE{0@b!=_Sg2^Ux zY{w0(nR)c$p3~SsxwI^&uCV}%z7oL$+ciBkJZ5aR0UA%cw-&;vy$|~_jFK`EQwP+( z4KwsI@xrjC-9h$FmJg-36Om%AG}fRW`H*+hfb)XOloW1Z3L7f{ub%JdpIJUgPA-bq6BG|q_Fs-h;T z>yDbjZIOs91!|4^+n`~dYA4JH+1LBHCNjabPA+o& z9XgjmGBm>#@WC!hDn9~uqaId;^A`Ny^~BNK8nF713ReS%2Grj5^oShCV@jF}dL?^s z*gf`7^qQ2G6bNwg(Sz&

      z4r*r|VPWM6hN{&7xv$RQO}Zn7RnRZ!)V9sRB$hC;}* KX);zY-!1|?!O3F) diff --git a/.github/scripts/publish_artifacts.sh b/.github/scripts/publish_artifacts.sh index f4a2f1734..cd1a5b75c 100755 --- a/.github/scripts/publish_artifacts.sh +++ b/.github/scripts/publish_artifacts.sh @@ -20,7 +20,7 @@ set -u gpg --quiet --batch --yes --decrypt --passphrase="${GPG_PRIVATE_KEY}" \ --output firebase.asc .github/resources/firebase.asc.gpg -gpg --import firebase.asc +gpg --import --no-tty --batch --yes firebase.asc # Does the following: # 1. Compiles the source (compile phase) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e1c307bc..985ce94d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,6 +91,14 @@ jobs: id: preflight run: ./.github/scripts/publish_preflight_check.sh + - name: Publish to Maven Central + run: ./.github/scripts/publish_artifacts.sh + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + NEXUS_OSSRH_USERNAME: ${{ secrets.NEXUS_OSSRH_USERNAME }} + NEXUS_OSSRH_PASSWORD: ${{ secrets.NEXUS_OSSRH_PASSWORD }} + # We pull this action from a custom fork of a contributor until # https://github.com/actions/create-release/pull/32 is merged. Also note that v1 of # this action does not support the "body" parameter. @@ -105,14 +113,6 @@ jobs: draft: false prerelease: false - - name: Publish to Maven Central - run: ./.github/scripts/publish_artifacts.sh - env: - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - NEXUS_OSSRH_USERNAME: ${{ secrets.NEXUS_OSSRH_USERNAME }} - NEXUS_OSSRH_PASSWORD: ${{ secrets.NEXUS_OSSRH_PASSWORD }} - # Post to Twitter if explicitly opted-in by adding the label 'release:tweet'. - name: Post to Twitter if: success() && From 68c05d7e19c4f2e91a14b650753019f15ef2e05a Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 14 May 2020 12:16:43 -0700 Subject: [PATCH 113/456] [chore] Release 6.13.0 take 3 (#415) --- .github/resources/settings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/resources/settings.xml b/.github/resources/settings.xml index 708bbcb75..4afbaca26 100644 --- a/.github/resources/settings.xml +++ b/.github/resources/settings.xml @@ -21,7 +21,7 @@ gpg - B652FFD3865AF7A75830876F5F55C8F6985BB9DD + A9B90B41060565F56F348F948B6B459CFD695DE8 ${env.GPG_PASSPHRASE} From 8c9608dbe300cc7d0c6ddff98e3ab51dc41c05ea Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 14 May 2020 12:31:01 -0700 Subject: [PATCH 114/456] [chore] Release 6.13.0 take 4 (#416) --- pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pom.xml b/pom.xml index ca1657fb8..6a2989169 100644 --- a/pom.xml +++ b/pom.xml @@ -174,6 +174,12 @@ sign + + + --pinentry-mode + loopback + + From a6b81e8b597d390cc9d4728ddaf54a2d6f3e0357 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 19 May 2020 10:59:49 -0700 Subject: [PATCH 115/456] chore: Updated integration tests to use non-public rules (#417) --- .../firebase/database/integration/OrderByTestIT.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java b/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java index f1cc99dca..2160406eb 100644 --- a/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import com.google.api.client.json.GenericJson; import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.database.ChildEventListener; @@ -66,7 +65,9 @@ public static void setUpClass() { @AfterClass public static void tearDownClass() throws IOException { - uploadRules(masterApp, "{\"rules\": {\".read\": true, \".write\": true}}"); + uploadRules( + masterApp, + "{\"rules\": {\".read\": \"auth != null\", \".write\": \"auth != null\"}}"); } @Before @@ -81,7 +82,8 @@ public void checkAndCleanupApp() { private static String formatRules(DatabaseReference ref, String rules) { return String.format( - "{\"rules\": {\".read\": true, \".write\": true, \"%s\": %s}}", ref.getKey(), rules); + "{\"rules\": {\".read\": \"auth != null\", \".write\": \"auth != null\", \"%s\": %s}}", + ref.getKey(), rules); } private static void uploadRules(FirebaseApp app, String rules) throws IOException { @@ -922,7 +924,7 @@ public void onComplete(DatabaseError error, DatabaseReference ref) { @Test public void testStartAtAndEndAtOnValueIndex() - throws InterruptedException, ExecutionException, TimeoutException, TestFailure, IOException { + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; Map initial = @@ -982,7 +984,7 @@ public void onChildAdded(DataSnapshot snapshot, String previousChildName) { @Test public void testRemovingDefaultListener() - throws InterruptedException, ExecutionException, TimeoutException, TestFailure, IOException { + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; Object initialData = MapBuilder.of("key", "value"); From b128b9bc03305c6ee9fae62787a2a63821b92c11 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 9 Jun 2020 12:03:01 -0700 Subject: [PATCH 116/456] chore: Upgraded GCP and other util dependencies (#438) --- pom.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 6a2989169..c663e2c7c 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.45.Final + 4.1.50.Final @@ -393,37 +393,37 @@ com.google.api-client google-api-client - 1.30.1 + 1.30.9 com.google.api-client google-api-client-gson - 1.30.1 + 1.30.9 com.google.http-client google-http-client - 1.30.1 + 1.35.0 com.google.api api-common - 1.8.1 + 1.9.2 com.google.auth google-auth-library-oauth2-http - 0.17.1 + 0.20.0 com.google.cloud google-cloud-storage - 1.91.0 + 1.108.0 com.google.cloud google-cloud-firestore - 1.31.0 + 1.34.0 From 3485c98f42d483b2336aa4357546f6d56d2a9346 Mon Sep 17 00:00:00 2001 From: Pavlos-Petros Tournaris Date: Tue, 9 Jun 2020 22:43:37 +0300 Subject: [PATCH 117/456] Add FcmOptions on MulticastMessage (#439) --- .../firebase/messaging/MulticastMessage.java | 15 ++++++++++++++- .../firebase/messaging/MulticastMessageTest.java | 5 ++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/MulticastMessage.java b/src/main/java/com/google/firebase/messaging/MulticastMessage.java index 96a6f19db..b15e58e5f 100644 --- a/src/main/java/com/google/firebase/messaging/MulticastMessage.java +++ b/src/main/java/com/google/firebase/messaging/MulticastMessage.java @@ -51,6 +51,7 @@ public class MulticastMessage { private final AndroidConfig androidConfig; private final WebpushConfig webpushConfig; private final ApnsConfig apnsConfig; + private final FcmOptions fcmOptions; private MulticastMessage(Builder builder) { this.tokens = builder.tokens.build(); @@ -64,6 +65,7 @@ private MulticastMessage(Builder builder) { this.androidConfig = builder.androidConfig; this.webpushConfig = builder.webpushConfig; this.apnsConfig = builder.apnsConfig; + this.fcmOptions = builder.fcmOptions; } List getMessageList() { @@ -71,7 +73,8 @@ List getMessageList() { .setNotification(this.notification) .setAndroidConfig(this.androidConfig) .setApnsConfig(this.apnsConfig) - .setWebpushConfig(this.webpushConfig); + .setWebpushConfig(this.webpushConfig) + .setFcmOptions(this.fcmOptions); if (this.data != null) { builder.putAllData(this.data); } @@ -99,6 +102,7 @@ public static class Builder { private AndroidConfig androidConfig; private WebpushConfig webpushConfig; private ApnsConfig apnsConfig; + private FcmOptions fcmOptions; private Builder() {} @@ -170,6 +174,15 @@ public Builder setApnsConfig(ApnsConfig apnsConfig) { return this; } + /** + * Sets the {@link FcmOptions}, which can be overridden by the platform-specific {@code + * fcm_options} fields. + */ + public Builder setFcmOptions(FcmOptions fcmOptions) { + this.fcmOptions = fcmOptions; + return this; + } + /** * Adds the given key-value pair to the message as a data field. Key or the value may not be * null. diff --git a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java index 25018ed4c..1ea57d8d5 100644 --- a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java @@ -39,6 +39,7 @@ public class MulticastMessageTest { .putData("key", "value") .build(); private static final Notification NOTIFICATION = new Notification("title", "body"); + private static final FcmOptions FCM_OPTIONS = FcmOptions.withAnalyticsLabel("analytics_label"); @Test public void testMulticastMessage() { @@ -47,6 +48,7 @@ public void testMulticastMessage() { .setApnsConfig(APNS) .setWebpushConfig(WEBPUSH) .setNotification(NOTIFICATION) + .setFcmOptions(FCM_OPTIONS) .putData("key1", "value1") .putAllData(ImmutableMap.of("key2", "value2")) .addToken("token1") @@ -96,7 +98,8 @@ private void assertMessage(Message message, String expectedToken) { assertSame(APNS, message.getApnsConfig()); assertSame(WEBPUSH, message.getWebpushConfig()); assertSame(NOTIFICATION, message.getNotification()); + assertSame(FCM_OPTIONS, message.getFcmOptions()); assertEquals(ImmutableMap.of("key1", "value1", "key2", "value2"), message.getData()); assertEquals(expectedToken, message.getToken()); } -} \ No newline at end of file +} From 2fa81c701159265a9aaf8a15ca427254300a4e4c Mon Sep 17 00:00:00 2001 From: chong-shao <31256040+chong-shao@users.noreply.github.com> Date: Mon, 15 Jun 2020 13:55:46 -0700 Subject: [PATCH 118/456] Support direct_boot_ok parameter (#329) --- .../firebase/messaging/AndroidConfig.java | 14 +++++++++++ .../firebase/messaging/MessageTest.java | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index dc0a45526..d16190881 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -51,6 +51,9 @@ public class AndroidConfig { @Key("fcm_options") private final AndroidFcmOptions fcmOptions; + + @Key("direct_boot_ok") + private final Boolean directBootOk; private AndroidConfig(Builder builder) { this.collapseKey = builder.collapseKey; @@ -75,6 +78,7 @@ private AndroidConfig(Builder builder) { this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data); this.notification = builder.notification; this.fcmOptions = builder.fcmOptions; + this.directBootOk = builder.directBootOk; } /** @@ -103,6 +107,7 @@ public static class Builder { private final Map data = new HashMap<>(); private AndroidNotification notification; private AndroidFcmOptions fcmOptions; + private Boolean directBootOk; private Builder() {} @@ -201,6 +206,15 @@ public Builder setFcmOptions(AndroidFcmOptions androidFcmOptions) { return this; } + /** + * Sets the direct_boot_ok flag, If set to true, messages will be allowed to be delivered to + * the app while the device is in direct boot mode. + */ + public Builder setDirectBootOk(boolean directBootOk) { + this.directBootOk = directBootOk; + return this; + } + /** * Creates a new {@link AndroidConfig} instance from the parameters set on this builder. * diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 778b4f109..c97fb6b67 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -200,6 +200,29 @@ public void testAndroidMessageWithNotification() throws IOException { assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); } + @Test + public void testAndroidMessageWithDirectBootOk() throws IOException { + Message message = Message.builder() + .setAndroidConfig(AndroidConfig.builder() + .setDirectBootOk(true) + .setNotification(AndroidNotification.builder() + .setTitle("android-title") + .setBody("android-body") + .build()) + .build()) + .setTopic("test-topic") + .build(); + Map notification = ImmutableMap.builder() + .put("title", "android-title") + .put("body", "android-body") + .build(); + Map data = ImmutableMap.of( + "direct_boot_ok", true, + "notification", notification + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); + } + @Test(expected = IllegalArgumentException.class) public void testAndroidNotificationWithNegativeCount() throws IllegalArgumentException { AndroidNotification.builder().setNotificationCount(-1).build(); From cc93b06796091de3207def69e8d23482fba52e7f Mon Sep 17 00:00:00 2001 From: rsgowman Date: Tue, 16 Jun 2020 14:25:14 -0400 Subject: [PATCH 119/456] Fixed a flaky auth integration test by retrying the GetUser() API call a few times with a small delay (#442) --- .../com/google/firebase/auth/FirebaseAuthIT.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index aa45f51b3..2915d1ddd 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -336,7 +336,20 @@ public void testLastRefreshTime() throws Exception { // Login to cause the lastRefreshTimestamp to be set. signInWithPassword(newUserRecord.getEmail(), "password"); - UserRecord userRecord = auth.getUser(newUserRecord.getUid()); + // Attempt to retrieve the user 3 times (with a small delay between each + // attempt). Occassionally, this call retrieves the user data without the + // lastLoginTime/lastRefreshTime set; possibly because it's hitting a + // different server than the login request uses. + UserRecord userRecord = null; + for (int i = 0; i < 3; i++) { + userRecord = auth.getUser(newUserRecord.getUid()); + + if (userRecord.getUserMetadata().getLastRefreshTimestamp() != 0) { + break; + } + + TimeUnit.SECONDS.sleep((long)Math.pow(2, i)); + } // Ensure the lastRefreshTimestamp is approximately "now" (with a tollerance of 10 minutes). long now = System.currentTimeMillis(); From ebb0c9475a4fa6ffabc245249dd81aee78b7bf45 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Wed, 17 Jun 2020 21:25:17 +0000 Subject: [PATCH 120/456] [chore] Release 6.14.0 (#444) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c663e2c7c..49e72ad8b 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.13.0 + 6.14.0 jar firebase-admin From f938b3b06fcc01141a7b2e706972bc7a9b970928 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Fri, 19 Jun 2020 14:39:56 -0400 Subject: [PATCH 121/456] Fix documentation strings in Android Notification (#445) * Fix doc strings * Fix code font * Fix punctuation --- .../java/com/google/firebase/messaging/AndroidConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index d16190881..888f00f0d 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -198,7 +198,7 @@ public Builder setNotification(AndroidNotification notification) { } /** - * Sets the {@link AndroidFcmOptions}, which will override values set in the {@link FcmOptions} + * Sets the {@link AndroidFcmOptions}, which overrides values set in the {@link FcmOptions} * for Android messages. */ public Builder setFcmOptions(AndroidFcmOptions androidFcmOptions) { @@ -207,7 +207,7 @@ public Builder setFcmOptions(AndroidFcmOptions androidFcmOptions) { } /** - * Sets the direct_boot_ok flag, If set to true, messages will be allowed to be delivered to + * Sets the {@code direct_boot_ok} flag. If set to true, messages are delivered to * the app while the device is in direct boot mode. */ public Builder setDirectBootOk(boolean directBootOk) { From eeee30b52bb6340aa1653da21645c34678e49070 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 10 Jul 2020 11:19:49 -0700 Subject: [PATCH 122/456] fix: Importing GCP dependencies via official BOMs (#451) * fix: Importing GCP dependencies via official BOMs * Upgraded api-client BOM to 1.30.10 --- pom.xml | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 49e72ad8b..e9a2fc038 100644 --- a/pom.xml +++ b/pom.xml @@ -388,49 +388,60 @@ + + + + com.google.cloud + libraries-bom + 8.0.0 + pom + import + + + com.google.api-client + google-api-client-bom + 1.30.10 + pom + import + + + + com.google.api-client google-api-client - 1.30.9 com.google.api-client google-api-client-gson - 1.30.9 com.google.http-client google-http-client - 1.35.0 com.google.api api-common - 1.9.2 com.google.auth google-auth-library-oauth2-http - 0.20.0 com.google.cloud google-cloud-storage - 1.108.0 com.google.cloud google-cloud-firestore - 1.34.0 com.google.guava guava - 26.0-android org.slf4j From 0a0662e61d91d40cbd725ffd70c7c0268f868cb9 Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Thu, 16 Jul 2020 17:25:21 -0400 Subject: [PATCH 123/456] feat(auth): Add tenant operations, tenant-aware user operations, and provider config operations (#395) * Pull parts of FirebaseAuth into an abstract class. (#352) This moves parts of FirebaseAuth into an abstract class as part of adding multi-tenancy support. * Add Tenant class and its create and update request classes. (#344) This pull request adds the Tenant class (including it's create/update inner classes) as part of adding multi-tenancy support. * Add ListTenantsPage class. (#358) Add ListTenantsPage and some supporting code as part of adding multi-tenancy support. This code was very largely based off of ListUsersPage and ListUsersPageTest. * Add updateRequest method to Tenant class and add unit tests. (#361) Added some things to the Tenant class and added a few unit tests. This is part of the initiative to adding multi-tenancy support (see issue #332). * Create TenantManager class and wire through listTenants operation. (#369) Add the TenantManager class and wire through the listTenants operation. Also add unit tests to FirebaseUserManagerTest. * Add deleteTenant operation to TenantManager. (#372) This adds deleteTenant to the TenantManager class. I've added the relevant unit tests to FirebaseUserManagerTest. This is part of the initiative to adding multi-tenancy support (see issue #332). * Add getTenant operation to TenantManager. (#371) Added getTenant to the TenantManager class. Also added the relevant unit tests to FirebaseUserManagerTest. This is part of the initiative to adding multi-tenancy support (see issue #332). * Add createTenant and updateTenant operations. (#377) Added createTenant and updateTenant to the TenantManager class. Also added the relevant unit tests to FirebaseUserManagerTest. This is part of the initiative to adding multi-tenancy support (see issue #332). * Add integration tests for TenantManager operations. (#385) This adds some integration testing for all of the tenant operations in TenantManager. Several bugs were uncovered after running the tests, so these have been fixed. This is part of the initiative to adding multi-tenancy support (see issue #332). * Add firebase auth destroy check before tenant operations. (#386) This addresses some TODOs left as part of the initiative to add multi-tenancy support (see issue #332). * Make user operations tenant-aware. (#387) This makes user operations tenant-aware. I've added some integration tests to ensure that this is working correctly. This is part of the initiative to adding multi-tenancy support (see issue #332). * Remove unused AutoValue dependency. (#392) Remove unused AutoValue dependency (and remove Java 8 API dependency which was accidentally introduced). * Indicate how to get set up for the multitenancy integration tests. (#393) This documentation is based off of the instructions in https://github.com/firebase/firebase-admin-node/blob/master/CONTRIBUTING.md. * Add tenant-aware token generation and verification. (#391) This incorporates the tenant ID into the token generation and validation when using a tenant-aware client. This is part of the initiative to add multi-tenancy support (see issue #332). * Fix javadoc comment. * Trigger CI * Make several Op methods private. * Move createSessionCookie and verifySessionCookie back to FirebaseAuth. * Make verifySessionCookieOp private. * Fix a few javadoc comments. * Address Kevin's feedback. * Make TenantAwareFirebaseAuth final. * chore: Merging master into tenant-mgt (#422) * Fixed a bad merge * Add provider config management operations. (#433) Adds all of the OIDC and SAML provider config operations, related to adding multi-tenancy support. * Stop using deprecated MockHttpTransport.builder() method. * Moved tenant management code into a new package (#449) * Multi-tenancy refactor experiment * fix(auth): Completed tenant mgt refactor * Added license header to new class * Responding to code review comments: Consolidated error codes in AuthHttpClient * Improve unit test coverage of tenant/provider-related code (#453) I've improved the unit test coverage of tenant/provider-related code, and I've also removed a number of unused imports. * Fix integration tests. --- CONTRIBUTING.md | 17 +- checkstyle.xml | 9 +- .../firebase/auth/AbstractFirebaseAuth.java | 1723 +++++++++++++++++ .../google/firebase/auth/FirebaseAuth.java | 1231 +----------- .../google/firebase/auth/FirebaseToken.java | 13 +- .../firebase/auth/FirebaseTokenUtils.java | 15 +- .../auth/FirebaseTokenVerifierImpl.java | 26 +- .../firebase/auth/FirebaseUserManager.java | 311 +-- .../auth/ListProviderConfigsPage.java | 268 +++ .../google/firebase/auth/ListUsersPage.java | 8 +- .../firebase/auth/OidcProviderConfig.java | 177 ++ .../google/firebase/auth/ProviderConfig.java | 157 ++ .../firebase/auth/SamlProviderConfig.java | 343 ++++ .../com/google/firebase/auth/UserRecord.java | 20 +- .../auth/internal/AuthHttpClient.java | 160 ++ .../internal/FirebaseCustomAuthToken.java | 13 + .../auth/internal/FirebaseTokenFactory.java | 13 +- .../auth/internal/GetAccountInfoResponse.java | 9 +- .../ListOidcProviderConfigsResponse.java | 62 + .../internal/ListProviderConfigsResponse.java | 32 + .../ListSamlProviderConfigsResponse.java | 62 + .../auth/internal/ListTenantsResponse.java | 55 + .../multitenancy/FirebaseTenantClient.java | 111 ++ .../auth/multitenancy/ListTenantsPage.java | 245 +++ .../firebase/auth/multitenancy/Tenant.java | 195 ++ .../multitenancy/TenantAwareFirebaseAuth.java | 50 + .../auth/multitenancy/TenantManager.java | 287 +++ .../google/firebase/auth/FirebaseAuthIT.java | 712 ++++--- .../firebase/auth/FirebaseAuthTest.java | 25 +- .../auth/FirebaseTokenVerifierImplTest.java | 51 + .../auth/FirebaseUserManagerTest.java | 1446 ++++++++++++-- .../com/google/firebase/auth/GetUsersIT.java | 14 +- .../auth/ListProviderConfigsPageTest.java | 376 ++++ .../firebase/auth/ListUsersPageTest.java | 33 +- .../firebase/auth/OidcProviderConfigTest.java | 160 ++ .../auth/ProviderConfigTestUtils.java | 125 ++ .../firebase/auth/SamlProviderConfigTest.java | 322 +++ .../google/firebase/auth/UserTestUtils.java | 124 ++ .../internal/FirebaseTokenFactoryTest.java | 20 + .../ListOidcProviderConfigsResponseTest.java | 70 + .../ListSamlProviderConfigsResponseTest.java | 70 + .../internal/ListTenantsResponseTest.java | 69 + .../FirebaseTenantClientTest.java | 363 ++++ .../multitenancy/ListTenantsPageTest.java | 349 ++++ .../TenantAwareFirebaseAuthIT.java | 450 +++++ .../auth/multitenancy/TenantManagerIT.java | 158 ++ .../auth/multitenancy/TenantTest.java | 92 + .../database/FirebaseDatabaseTest.java | 8 +- src/test/resources/getUser.json | 3 +- src/test/resources/listOidc.json | 15 + src/test/resources/listSaml.json | 35 + src/test/resources/listTenants.json | 17 + src/test/resources/listUsers.json | 6 +- src/test/resources/oidc.json | 7 + src/test/resources/saml.json | 17 + src/test/resources/tenant.json | 8 + 56 files changed, 9028 insertions(+), 1729 deletions(-) create mode 100644 src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java create mode 100644 src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java create mode 100644 src/main/java/com/google/firebase/auth/OidcProviderConfig.java create mode 100644 src/main/java/com/google/firebase/auth/ProviderConfig.java create mode 100644 src/main/java/com/google/firebase/auth/SamlProviderConfig.java create mode 100644 src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java create mode 100644 src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java create mode 100644 src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java create mode 100644 src/main/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponse.java create mode 100644 src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java create mode 100644 src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java create mode 100644 src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java create mode 100644 src/main/java/com/google/firebase/auth/multitenancy/Tenant.java create mode 100644 src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java create mode 100644 src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java create mode 100644 src/test/java/com/google/firebase/auth/ListProviderConfigsPageTest.java create mode 100644 src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java create mode 100644 src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java create mode 100644 src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java create mode 100644 src/test/java/com/google/firebase/auth/UserTestUtils.java create mode 100644 src/test/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponseTest.java create mode 100644 src/test/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponseTest.java create mode 100644 src/test/java/com/google/firebase/auth/internal/ListTenantsResponseTest.java create mode 100644 src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java create mode 100644 src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java create mode 100644 src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java create mode 100644 src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java create mode 100644 src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java create mode 100644 src/test/resources/listOidc.json create mode 100644 src/test/resources/listSaml.json create mode 100644 src/test/resources/listTenants.json create mode 100644 src/test/resources/oidc.json create mode 100644 src/test/resources/saml.json create mode 100644 src/test/resources/tenant.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11012bfff..c346c5b9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,8 +141,17 @@ Authentication Admin` role at [Google Cloud Platform Console / IAM & admin](https://console.cloud.google.com/iam-admin). This is required to ensure that exported user records contain the password hashes of the user accounts. Also obtain the web API key of the project from the "Settings > General" page, and save it as -`integration_apikey.txt` at the root of the codebase. Now run the following command to invoke the -integration test suite: +`integration_apikey.txt` at the root of the codebase. + +Some of the integration tests require an +[Identity Platform](https://cloud.google.com/identity-platform/) project with multi-tenancy +[enabled](https://cloud.google.com/identity-platform/docs/multi-tenancy-quickstart#enabling_multi-tenancy). +An existing Firebase project can be upgraded to an Identity Platform project without losing any +functionality via the +[Identity Platform Marketplace Page](https://console.cloud.google.com/customer-identity). Note that +charges may be incurred for active users beyond the Identity Platform free tier. + +Now run the following command to invoke the integration test suite: ``` mvn verify @@ -153,14 +162,14 @@ tests, specify the `-DskipUTs` flag. ### Generating API Docs -Invoke the [Maven Javadoc plugin](https://maven.apache.org/plugins/maven-javadoc-plugin/) as +Invoke the [Maven Javadoc plugin](https://maven.apache.org/plugins/maven-javadoc-plugin/) as follows to generate API docs for all packages in the codebase: ``` mvn javadoc:javadoc ``` -This will generate the API docs, and place them in the `target/site/apidocs` directory. +This will generate the API docs, and place them in the `target/site/apidocs` directory. To generate API docs for only the public APIs (i.e. ones that are not marked with `@hide` tags), you need to trigger the `devsite-apidocs` Maven profile. This profile uses Maven Javadoc plugin diff --git a/checkstyle.xml b/checkstyle.xml index 663918173..77e2dba54 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -42,15 +42,15 @@ - + - - + + @@ -229,6 +229,9 @@ + + + diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java new file mode 100644 index 000000000..ad30d2cc3 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -0,0 +1,1723 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import com.google.api.client.json.JsonFactory; +import com.google.api.client.util.Clock; +import com.google.api.core.ApiFuture; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; +import com.google.firebase.auth.FirebaseUserManager.UserImportRequest; +import com.google.firebase.auth.ListProviderConfigsPage.DefaultOidcProviderConfigSource; +import com.google.firebase.auth.ListProviderConfigsPage.DefaultSamlProviderConfigSource; +import com.google.firebase.auth.ListUsersPage.DefaultUserSource; +import com.google.firebase.auth.internal.FirebaseTokenFactory; +import com.google.firebase.internal.CallableOperation; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This is the abstract class for server-side Firebase Authentication actions. + */ +public abstract class AbstractFirebaseAuth { + + private static final String ERROR_CUSTOM_TOKEN = "ERROR_CUSTOM_TOKEN"; + + private final Object lock = new Object(); + private final AtomicBoolean destroyed = new AtomicBoolean(false); + + private final FirebaseApp firebaseApp; + private final Supplier tokenFactory; + private final Supplier idTokenVerifier; + private final Supplier cookieVerifier; + private final Supplier userManager; + private final JsonFactory jsonFactory; + + protected AbstractFirebaseAuth(Builder builder) { + this.firebaseApp = checkNotNull(builder.firebaseApp); + this.tokenFactory = threadSafeMemoize(builder.tokenFactory); + this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier); + this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier); + this.userManager = threadSafeMemoize(builder.userManager); + this.jsonFactory = builder.firebaseApp.getOptions().getJsonFactory(); + } + + protected static Builder builderFromAppAndTenantId(final FirebaseApp app, final String tenantId) { + return AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setTokenFactory( + new Supplier() { + @Override + public FirebaseTokenFactory get() { + return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId); + } + }) + .setIdTokenVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId); + } + }) + .setCookieVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); + } + }) + .setUserManager( + new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager + .builder() + .setFirebaseApp(app) + .setTenantId(tenantId) + .build(); + } + }); + } + + /** + * Creates a Firebase custom token for the given UID. This token can then be sent back to a client + * application to be used with the signInWithCustomToken + * authentication API. + * + *

      {@link FirebaseApp} must have been initialized with service account credentials to use call + * this method. + * + * @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. + * @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 FirebaseAuthException If an error occurs while generating the custom token. + */ + public String createCustomToken(@NonNull String uid) throws FirebaseAuthException { + return createCustomToken(uid, null); + } + + /** + * Creates a Firebase custom token for the given UID, containing the specified additional claims. + * This token can then be sent back to a client application to be used with the signInWithCustomToken + * authentication API. + * + *

      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. + * @param developerClaims Additional claims to be stored in the token (and made available to + * 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. + * @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, @Nullable Map developerClaims) + throws FirebaseAuthException { + return createCustomTokenOp(uid, developerClaims).call(); + } + + /** + * Similar to {@link #createCustomToken(String)} but performs the operation asynchronously. + * + * @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. + * @return An {@code ApiFuture} which will complete successfully with the created Firebase custom + * token, or unsuccessfully with the failure Exception. + * @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not + * been initialized with service account credentials. + */ + public ApiFuture createCustomTokenAsync(@NonNull String uid) { + return createCustomTokenAsync(uid, null); + } + + /** + * Similar to {@link #createCustomToken(String, Map)} but performs the operation asynchronously. + * + * @param uid The UID to store in the token. This identifies the user to other Firebase services + * (Realtime Database, Storage, etc.). Should be less than 128 characters. + * @param developerClaims Additional claims to be stored in the token (and made available to + * 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 An {@code ApiFuture} which will complete successfully with the created Firebase custom + * token, or unsuccessfully with the failure Exception. + * @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not + * been initialized with service account credentials. + */ + public ApiFuture createCustomTokenAsync( + @NonNull String uid, @Nullable Map developerClaims) { + return createCustomTokenOp(uid, developerClaims).callAsync(firebaseApp); + } + + private CallableOperation createCustomTokenOp( + final String uid, final Map developerClaims) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseTokenFactory tokenFactory = this.tokenFactory.get(); + return new CallableOperation() { + @Override + public String execute() throws FirebaseAuthException { + try { + return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims); + } catch (IOException e) { + throw new FirebaseAuthException( + ERROR_CUSTOM_TOKEN, "Failed to generate a custom token", e); + } + } + }; + } + + /** + * Parses and verifies a Firebase ID Token. + * + *

      A Firebase application can identify itself to a trusted backend server by sending its + * Firebase ID Token (accessible via the {@code getToken} API in the Firebase Authentication + * client) with its requests. The backend server can then use the {@code verifyIdToken()} method + * to verify that the token is valid. This method ensures that the token is correctly signed, has + * not expired, and it was issued to the Firebase project associated with this {@link + * FirebaseAuth} instance. + * + *

      This method does not check whether a token has been revoked. Use {@link + * #verifyIdToken(String, boolean)} to perform an additional revocation check. + * + * @param idToken A Firebase ID token string to parse and verify. + * @return A {@link FirebaseToken} representing the verified and decoded token. + * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} + * instance does not have a project ID associated with it. + * @throws FirebaseAuthException If an error occurs while parsing or validating the token. + */ + public FirebaseToken verifyIdToken(@NonNull String idToken) throws FirebaseAuthException { + return verifyIdToken(idToken, false); + } + + /** + * Parses and verifies a Firebase ID Token. + * + *

      A Firebase application can identify itself to a trusted backend server by sending its + * Firebase ID Token (accessible via the {@code getToken} API in the Firebase Authentication + * client) with its requests. The backend server can then use the {@code verifyIdToken()} method + * to verify that the token is valid. This method ensures that the token is correctly signed, has + * not expired, and it was issued to the Firebase project associated with this {@link + * FirebaseAuth} instance. + * + *

      If {@code checkRevoked} is set to true, this method performs an additional check to see if + * the ID token has been revoked since it was issues. This requires making an additional remote + * API call. + * + * @param idToken A Firebase ID token string to parse and verify. + * @param checkRevoked A boolean denoting whether to check if the tokens were revoked. + * @return A {@link FirebaseToken} representing the verified and decoded token. + * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} + * instance does not have a project ID associated with it. + * @throws FirebaseAuthException If an error occurs while parsing or validating the token. + */ + public FirebaseToken verifyIdToken(@NonNull String idToken, boolean checkRevoked) + throws FirebaseAuthException { + return verifyIdTokenOp(idToken, checkRevoked).call(); + } + + /** + * Similar to {@link #verifyIdToken(String)} but performs the operation asynchronously. + * + * @param idToken A Firebase ID Token to verify and parse. + * @return An {@code ApiFuture} which will complete successfully with the parsed token, or + * unsuccessfully with a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} + * instance does not have a project ID associated with it. + */ + public ApiFuture verifyIdTokenAsync(@NonNull String idToken) { + return verifyIdTokenAsync(idToken, false); + } + + /** + * Similar to {@link #verifyIdToken(String, boolean)} but performs the operation asynchronously. + * + * @param idToken A Firebase ID Token to verify and parse. + * @param checkRevoked A boolean denoting whether to check if the tokens were revoked. + * @return An {@code ApiFuture} which will complete successfully with the parsed token, or + * unsuccessfully with a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} + * instance does not have a project ID associated with it. + */ + public ApiFuture + verifyIdTokenAsync(@NonNull String idToken, boolean checkRevoked) { + return verifyIdTokenOp(idToken, checkRevoked).callAsync(firebaseApp); + } + + private CallableOperation verifyIdTokenOp( + final String idToken, final boolean checkRevoked) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(idToken), "ID token must not be null or empty"); + final FirebaseTokenVerifier verifier = getIdTokenVerifier(checkRevoked); + return new CallableOperation() { + @Override + protected FirebaseToken execute() throws FirebaseAuthException { + return verifier.verifyToken(idToken); + } + }; + } + + @VisibleForTesting + FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) { + FirebaseTokenVerifier verifier = idTokenVerifier.get(); + if (checkRevoked) { + FirebaseUserManager userManager = getUserManager(); + verifier = RevocationCheckDecorator.decorateIdTokenVerifier(verifier, userManager); + } + return verifier; + } + + /** + * Revokes all refresh tokens for the specified user. + * + *

      Updates the user's tokensValidAfterTimestamp to the current UTC time expressed in + * milliseconds since the epoch and truncated to 1 second accuracy. It is important that the + * server on which this is called has its clock set correctly and synchronized. + * + *

      While this will revoke all sessions for a specified user and disable any new ID tokens for + * existing sessions from getting minted, existing ID tokens may remain active until their natural + * expiration (one hour). To verify that ID tokens are revoked, use {@link + * #verifyIdTokenAsync(String, boolean)}. + * + * @param uid The user id for which tokens are revoked. + * @throws IllegalArgumentException If the user ID is null or empty. + * @throws FirebaseAuthException If an error occurs while revoking tokens. + */ + public void revokeRefreshTokens(@NonNull String uid) throws FirebaseAuthException { + revokeRefreshTokensOp(uid).call(); + } + + /** + * Similar to {@link #revokeRefreshTokens(String)} but performs the operation asynchronously. + * + * @param uid The user id for which tokens are revoked. + * @return An {@code ApiFuture} which will complete successfully or fail with a {@link + * FirebaseAuthException} in the event of an error. + * @throws IllegalArgumentException If the user ID is null or empty. + */ + public ApiFuture revokeRefreshTokensAsync(@NonNull String uid) { + return revokeRefreshTokensOp(uid).callAsync(firebaseApp); + } + + private CallableOperation revokeRefreshTokensOp(final String uid) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + int currentTimeSeconds = (int) (System.currentTimeMillis() / 1000); + UserRecord.UpdateRequest request = + new UserRecord.UpdateRequest(uid).setValidSince(currentTimeSeconds); + userManager.updateUser(request, jsonFactory); + return null; + } + }; + } + + /** + * Gets the user data corresponding to the specified user ID. + * + * @param uid A user ID string. + * @return A {@link UserRecord} instance. + * @throws IllegalArgumentException If the user ID string is null or empty. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public UserRecord getUser(@NonNull String uid) throws FirebaseAuthException { + return getUserOp(uid).call(); + } + + /** + * Similar to {@link #getUser(String)} but performs the operation asynchronously. + * + * @param uid A user ID string. + * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} + * instance. If an error occurs while retrieving user data or if the specified user ID does + * not exist, the future throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the user ID string is null or empty. + */ + public ApiFuture getUserAsync(@NonNull String uid) { + return getUserOp(uid).callAsync(firebaseApp); + } + + private CallableOperation getUserOp(final String uid) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected UserRecord execute() throws FirebaseAuthException { + return userManager.getUserById(uid); + } + }; + } + + /** + * Gets the user data corresponding to the specified user email. + * + * @param email A user email address string. + * @return A {@link UserRecord} instance. + * @throws IllegalArgumentException If the email is null or empty. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public UserRecord getUserByEmail(@NonNull String email) throws FirebaseAuthException { + return getUserByEmailOp(email).call(); + } + + /** + * Similar to {@link #getUserByEmail(String)} but performs the operation asynchronously. + * + * @param email A user email address string. + * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} + * instance. If an error occurs while retrieving user data or if the email address does not + * correspond to a user, the future throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the email is null or empty. + */ + public ApiFuture getUserByEmailAsync(@NonNull String email) { + return getUserByEmailOp(email).callAsync(firebaseApp); + } + + private CallableOperation getUserByEmailOp( + final String email) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected UserRecord execute() throws FirebaseAuthException { + return userManager.getUserByEmail(email); + } + }; + } + + /** + * Gets the user data corresponding to the specified user phone number. + * + * @param phoneNumber A user phone number string. + * @return A a {@link UserRecord} instance. + * @throws IllegalArgumentException If the phone number is null or empty. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public UserRecord getUserByPhoneNumber(@NonNull String phoneNumber) throws FirebaseAuthException { + return getUserByPhoneNumberOp(phoneNumber).call(); + } + + /** + * Gets the user data corresponding to the specified user phone number. + * + * @param phoneNumber A user phone number string. + * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} + * instance. If an error occurs while retrieving user data or if the phone number does not + * correspond to a user, the future throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the phone number is null or empty. + */ + public ApiFuture getUserByPhoneNumberAsync(@NonNull String phoneNumber) { + return getUserByPhoneNumberOp(phoneNumber).callAsync(firebaseApp); + } + + private CallableOperation getUserByPhoneNumberOp( + final String phoneNumber) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected UserRecord execute() throws FirebaseAuthException { + return userManager.getUserByPhoneNumber(phoneNumber); + } + }; + } + + /** + * Gets a page of users starting from the specified {@code pageToken}. Page size is limited to + * 1000 users. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of users. + * @return A {@link ListUsersPage} instance. + * @throws IllegalArgumentException If the specified page token is empty. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public ListUsersPage listUsers(@Nullable String pageToken) throws FirebaseAuthException { + return listUsers(pageToken, FirebaseUserManager.MAX_LIST_USERS_RESULTS); + } + + /** + * Gets a page of users starting from the specified {@code pageToken}. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of users. + * @param maxResults Maximum number of users to include in the returned page. This may not exceed + * 1000. + * @return A {@link ListUsersPage} instance. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public ListUsersPage listUsers(@Nullable String pageToken, int maxResults) + throws FirebaseAuthException { + return listUsersOp(pageToken, maxResults).call(); + } + + /** + * Similar to {@link #listUsers(String)} but performs the operation asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of users. + * @return An {@code ApiFuture} which will complete successfully with a {@link ListUsersPage} + * instance. If an error occurs while retrieving user data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty. + */ + public ApiFuture listUsersAsync(@Nullable String pageToken) { + return listUsersAsync(pageToken, FirebaseUserManager.MAX_LIST_USERS_RESULTS); + } + + /** + * Similar to {@link #listUsers(String, int)} but performs the operation asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of users. + * @param maxResults Maximum number of users to include in the returned page. This may not exceed + * 1000. + * @return An {@code ApiFuture} which will complete successfully with a {@link ListUsersPage} + * instance. If an error occurs while retrieving user data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + */ + public ApiFuture listUsersAsync(@Nullable String pageToken, int maxResults) { + return listUsersOp(pageToken, maxResults).callAsync(firebaseApp); + } + + private CallableOperation listUsersOp( + @Nullable final String pageToken, final int maxResults) { + checkNotDestroyed(); + final FirebaseUserManager userManager = getUserManager(); + final DefaultUserSource source = new DefaultUserSource(userManager, jsonFactory); + final ListUsersPage.Factory factory = new ListUsersPage.Factory(source, maxResults, pageToken); + return new CallableOperation() { + @Override + protected ListUsersPage execute() throws FirebaseAuthException { + return factory.create(); + } + }; + } + + /** + * Creates a new user account with the attributes contained in the specified {@link + * UserRecord.CreateRequest}. + * + * @param request A non-null {@link UserRecord.CreateRequest} instance. + * @return A {@link UserRecord} instance corresponding to the newly created account. + * @throws NullPointerException if the provided request is null. + * @throws FirebaseAuthException if an error occurs while creating the user account. + */ + public UserRecord createUser(@NonNull UserRecord.CreateRequest request) + throws FirebaseAuthException { + return createUserOp(request).call(); + } + + /** + * Similar to {@link #createUser} but performs the operation asynchronously. + * + * @param request A non-null {@link UserRecord.CreateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} + * instance corresponding to the newly created account. If an error occurs while creating the + * user account, the future throws a {@link FirebaseAuthException}. + * @throws NullPointerException if the provided request is null. + */ + public ApiFuture createUserAsync(@NonNull UserRecord.CreateRequest request) { + return createUserOp(request).callAsync(firebaseApp); + } + + private CallableOperation createUserOp( + final UserRecord.CreateRequest request) { + checkNotDestroyed(); + checkNotNull(request, "create request must not be null"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected UserRecord execute() throws FirebaseAuthException { + String uid = userManager.createUser(request); + return userManager.getUserById(uid); + } + }; + } + + /** + * Updates an existing user account with the attributes contained in the specified {@link + * UserRecord.UpdateRequest}. + * + * @param request A non-null {@link UserRecord.UpdateRequest} instance. + * @return A {@link UserRecord} instance corresponding to the updated user account. + * @throws NullPointerException if the provided update request is null. + * @throws FirebaseAuthException if an error occurs while updating the user account. + */ + public UserRecord updateUser(@NonNull UserRecord.UpdateRequest request) + throws FirebaseAuthException { + return updateUserOp(request).call(); + } + + /** + * Similar to {@link #updateUser} but performs the operation asynchronously. + * + * @param request A non-null {@link UserRecord.UpdateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} + * instance corresponding to the updated user account. If an error occurs while updating the + * user account, the future throws a {@link FirebaseAuthException}. + */ + public ApiFuture updateUserAsync(@NonNull UserRecord.UpdateRequest request) { + return updateUserOp(request).callAsync(firebaseApp); + } + + private CallableOperation updateUserOp( + final UserRecord.UpdateRequest request) { + checkNotDestroyed(); + checkNotNull(request, "update request must not be null"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected UserRecord execute() throws FirebaseAuthException { + userManager.updateUser(request, jsonFactory); + return userManager.getUserById(request.getUid()); + } + }; + } + + /** + * Sets the specified custom claims on an existing user account. A null claims value removes any + * claims currently set on the user account. The claims should serialize into a valid JSON string. + * The serialized claims must not be larger than 1000 characters. + * + * @param uid A user ID string. + * @param claims A map of custom claims or null. + * @throws FirebaseAuthException If an error occurs while updating custom claims. + * @throws IllegalArgumentException If the user ID string is null or empty, or the claims payload + * is invalid or too large. + */ + public void setCustomUserClaims(@NonNull String uid, @Nullable Map claims) + throws FirebaseAuthException { + setCustomUserClaimsOp(uid, claims).call(); + } + + /** + * @deprecated Use {@link #setCustomUserClaims(String, Map)} instead. + */ + public void setCustomClaims(@NonNull String uid, @Nullable Map claims) + throws FirebaseAuthException { + setCustomUserClaims(uid, claims); + } + + /** + * Similar to {@link #setCustomUserClaims(String, Map)} but performs the operation asynchronously. + * + * @param uid A user ID string. + * @param claims A map of custom claims or null. + * @return An {@code ApiFuture} which will complete successfully when the user account has been + * updated. If an error occurs while deleting the user account, the future throws a {@link + * FirebaseAuthException}. + * @throws IllegalArgumentException If the user ID string is null or empty. + */ + public ApiFuture setCustomUserClaimsAsync( + @NonNull String uid, @Nullable Map claims) { + return setCustomUserClaimsOp(uid, claims).callAsync(firebaseApp); + } + + private CallableOperation setCustomUserClaimsOp( + final String uid, final Map claims) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + final UserRecord.UpdateRequest request = + new UserRecord.UpdateRequest(uid).setCustomClaims(claims); + userManager.updateUser(request, jsonFactory); + return null; + } + }; + } + + /** + * Deletes the user identified by the specified user ID. + * + * @param uid A user ID string. + * @throws IllegalArgumentException If the user ID string is null or empty. + * @throws FirebaseAuthException If an error occurs while deleting the user. + */ + public void deleteUser(@NonNull String uid) throws FirebaseAuthException { + deleteUserOp(uid).call(); + } + + /** + * Similar to {@link #deleteUser(String)} but performs the operation asynchronously. + * + * @param uid A user ID string. + * @return An {@code ApiFuture} which will complete successfully when the specified user account + * has been deleted. If an error occurs while deleting the user account, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the user ID string is null or empty. + */ + public ApiFuture deleteUserAsync(String uid) { + return deleteUserOp(uid).callAsync(firebaseApp); + } + + private CallableOperation deleteUserOp(final String uid) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + userManager.deleteUser(uid); + return null; + } + }; + } + + /** + * Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a + * time. This operation is optimized for bulk imports and will ignore checks on identifier + * uniqueness which could result in duplications. + * + *

      {@link UserImportOptions} is required to import users with passwords. See {@link + * #importUsers(List, UserImportOptions)}. + * + * @param users A non-empty list of users to be imported. Length must not exceed 1000. + * @return A {@link UserImportResult} instance. + * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 + * elements. Or if at least one user specifies a password. + * @throws FirebaseAuthException If an error occurs while importing users. + */ + public UserImportResult importUsers(List users) throws FirebaseAuthException { + return importUsers(users, null); + } + + /** + * Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a + * time. This operation is optimized for bulk imports and will ignore checks on identifier + * uniqueness which could result in duplications. + * + * @param users A non-empty list of users to be imported. Length must not exceed 1000. + * @param options a {@link UserImportOptions} instance or null. Required when importing users with + * passwords. + * @return A {@link UserImportResult} instance. + * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 + * elements. Or if at least one user specifies a password, and options is null. + * @throws FirebaseAuthException If an error occurs while importing users. + */ + public UserImportResult importUsers( + List users, @Nullable UserImportOptions options) + throws FirebaseAuthException { + return importUsersOp(users, options).call(); + } + + /** + * Similar to {@link #importUsers(List)} but performs the operation asynchronously. + * + * @param users A non-empty list of users to be imported. Length must not exceed 1000. + * @return An {@code ApiFuture} which will complete successfully when the user accounts are + * imported. If an error occurs while importing the users, the future throws a {@link + * FirebaseAuthException}. + * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 + * elements. Or if at least one user specifies a password. + */ + public ApiFuture importUsersAsync(List users) { + return importUsersAsync(users, null); + } + + /** + * Similar to {@link #importUsers(List, UserImportOptions)} but performs the operation + * asynchronously. + * + * @param users A non-empty list of users to be imported. Length must not exceed 1000. + * @param options a {@link UserImportOptions} instance or null. Required when importing users with + * passwords. + * @return An {@code ApiFuture} which will complete successfully when the user accounts are + * imported. If an error occurs while importing the users, the future throws a {@link + * FirebaseAuthException}. + * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 + * elements. Or if at least one user specifies a password, and options is null. + */ + public ApiFuture importUsersAsync( + List users, @Nullable UserImportOptions options) { + return importUsersOp(users, options).callAsync(firebaseApp); + } + + private CallableOperation importUsersOp( + final List users, final UserImportOptions options) { + checkNotDestroyed(); + final UserImportRequest request = new UserImportRequest(users, options, jsonFactory); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected UserImportResult execute() throws FirebaseAuthException { + return userManager.importUsers(request); + } + }; + } + + /** + * Gets the user data corresponding to the specified identifiers. + * + *

      There are no ordering guarantees; in particular, the nth entry in the users result list is + * not guaranteed to correspond to the nth entry in the input parameters list. + * + *

      A maximum of 100 identifiers may be specified. If more than 100 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + * @param identifiers The identifiers used to indicate which user records should be returned. Must + * have 100 or fewer entries. + * @return The corresponding user records. + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 + * identifiers are specified. + * @throws NullPointerException If the identifiers parameter is null. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public GetUsersResult getUsers(@NonNull Collection identifiers) + throws FirebaseAuthException { + return getUsersOp(identifiers).call(); + } + + /** + * Gets the user data corresponding to the specified identifiers. + * + *

      There are no ordering guarantees; in particular, the nth entry in the users result list is + * not guaranteed to correspond to the nth entry in the input parameters list. + * + *

      A maximum of 100 identifiers may be specified. If more than 100 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + * @param identifiers The identifiers used to indicate which user records should be returned. + * Must have 100 or fewer entries. + * @return An {@code ApiFuture} that resolves to the corresponding user records. + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 + * identifiers are specified. + * @throws NullPointerException If the identifiers parameter is null. + */ + public ApiFuture getUsersAsync(@NonNull Collection identifiers) { + return getUsersOp(identifiers).callAsync(firebaseApp); + } + + private CallableOperation getUsersOp( + @NonNull final Collection identifiers) { + checkNotDestroyed(); + checkNotNull(identifiers, "identifiers must not be null"); + checkArgument(identifiers.size() <= FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE, + "identifiers parameter must have <= " + FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE + + " entries."); + + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected GetUsersResult execute() throws FirebaseAuthException { + Set users = userManager.getAccountInfo(identifiers); + Set notFound = new HashSet<>(); + for (UserIdentifier id : identifiers) { + if (!isUserFound(id, users)) { + notFound.add(id); + } + } + return new GetUsersResult(users, notFound); + } + }; + } + + private boolean isUserFound(UserIdentifier id, Collection userRecords) { + for (UserRecord userRecord : userRecords) { + if (id.matches(userRecord)) { + return true; + } + } + return false; + } + + /** + * Deletes the users specified by the given identifiers. + * + *

      Deleting a non-existing user does not generate an error (the method is idempotent). + * Non-existing users are considered to be successfully deleted and are therefore included in the + * DeleteUsersResult.getSuccessCount() value. + * + *

      A maximum of 1000 identifiers may be supplied. If more than 1000 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + *

      This API has a rate limit of 1 QPS. Exceeding the limit may result in a quota exceeded + * error. If you want to delete more than 1000 users, we suggest adding a delay to ensure you + * don't exceed this limit. + * + * @param uids The uids of the users to be deleted. Must have <= 1000 entries. + * @return The total number of successful/failed deletions, as well as the array of errors that + * correspond to the failed deletions. + * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * identifiers are specified. + * @throws FirebaseAuthException If an error occurs while deleting users. + */ + public DeleteUsersResult deleteUsers(List uids) throws FirebaseAuthException { + return deleteUsersOp(uids).call(); + } + + /** + * Similar to {@link #deleteUsers(List)} but performs the operation asynchronously. + * + * @param uids The uids of the users to be deleted. Must have <= 1000 entries. + * @return An {@code ApiFuture} that resolves to the total number of successful/failed + * deletions, as well as the array of errors that correspond to the failed deletions. If an + * error occurs while deleting the user account, the future throws a + * {@link FirebaseAuthException}. + * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * identifiers are specified. + */ + public ApiFuture deleteUsersAsync(List uids) { + return deleteUsersOp(uids).callAsync(firebaseApp); + } + + private CallableOperation deleteUsersOp( + final List uids) { + checkNotDestroyed(); + checkNotNull(uids, "uids must not be null"); + for (String uid : uids) { + UserRecord.checkUid(uid); + } + checkArgument(uids.size() <= FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE, + "uids parameter must have <= " + FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE + + " entries."); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected DeleteUsersResult execute() throws FirebaseAuthException { + return userManager.deleteUsers(uids); + } + }; + } + + /** + * 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 object 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. + * @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 object 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. + * @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 object 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. + * @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 object 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. + * @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 object 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. + * @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"); + } + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected String execute() throws FirebaseAuthException { + return userManager.getEmailActionLink(type, email, settings); + } + }; + } + + /** + * Creates a new OpenID Connect auth provider config with the attributes contained in the + * specified {@link OidcProviderConfig.CreateRequest}. + * + * @param request A non-null {@link OidcProviderConfig.CreateRequest} instance. + * @return An {@link OidcProviderConfig} instance corresponding to the newly created provider + * config. + * @throws NullPointerException if the provided request is null. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not + * prefixed with 'oidc.'. + * @throws FirebaseAuthException if an error occurs while creating the provider config. + */ + public OidcProviderConfig createOidcProviderConfig( + @NonNull OidcProviderConfig.CreateRequest request) throws FirebaseAuthException { + return createOidcProviderConfigOp(request).call(); + } + + /** + * Similar to {@link #createOidcProviderConfig} but performs the operation asynchronously. + * + * @param request A non-null {@link OidcProviderConfig.CreateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link OidcProviderConfig} + * instance corresponding to the newly created provider config. If an error occurs while + * creating the provider config, the future throws a {@link FirebaseAuthException}. + * @throws NullPointerException if the provided request is null. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not + * prefixed with 'oidc.'. + */ + public ApiFuture createOidcProviderConfigAsync( + @NonNull OidcProviderConfig.CreateRequest request) { + return createOidcProviderConfigOp(request).callAsync(firebaseApp); + } + + private CallableOperation + createOidcProviderConfigOp(final OidcProviderConfig.CreateRequest request) { + checkNotDestroyed(); + checkNotNull(request, "Create request must not be null."); + OidcProviderConfig.checkOidcProviderId(request.getProviderId()); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected OidcProviderConfig execute() throws FirebaseAuthException { + return userManager.createOidcProviderConfig(request); + } + }; + } + + /** + * Updates an existing OpenID Connect auth provider config with the attributes contained in the + * specified {@link OidcProviderConfig.UpdateRequest}. + * + * @param request A non-null {@link OidcProviderConfig.UpdateRequest} instance. + * @return A {@link OidcProviderConfig} instance corresponding to the updated provider config. + * @throws NullPointerException if the provided update request is null. + * @throws IllegalArgumentException If the provided update request is invalid. + * @throws FirebaseAuthException if an error occurs while updating the provider config. + */ + public OidcProviderConfig updateOidcProviderConfig( + @NonNull OidcProviderConfig.UpdateRequest request) throws FirebaseAuthException { + return updateOidcProviderConfigOp(request).call(); + } + + /** + * Similar to {@link #updateOidcProviderConfig} but performs the operation asynchronously. + * + * @param request A non-null {@link OidcProviderConfig.UpdateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link OidcProviderConfig} + * instance corresponding to the updated provider config. If an error occurs while updating + * the provider config, the future throws a {@link FirebaseAuthException}. + * @throws NullPointerException if the provided update request is null. + * @throws IllegalArgumentException If the provided update request is invalid. + */ + public ApiFuture updateOidcProviderConfigAsync( + @NonNull OidcProviderConfig.UpdateRequest request) { + return updateOidcProviderConfigOp(request).callAsync(firebaseApp); + } + + private CallableOperation updateOidcProviderConfigOp( + final OidcProviderConfig.UpdateRequest request) { + checkNotDestroyed(); + checkNotNull(request, "Update request must not be null."); + checkArgument(!request.getProperties().isEmpty(), + "Update request must have at least one property set."); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected OidcProviderConfig execute() throws FirebaseAuthException { + return userManager.updateOidcProviderConfig(request); + } + }; + } + + /** + * Gets the OpenID Connect auth provider corresponding to the specified provider ID. + * + * @param providerId A provider ID string. + * @return An {@link OidcProviderConfig} instance. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with 'oidc'. + * @throws FirebaseAuthException If an error occurs while retrieving the provider config. + */ + public OidcProviderConfig getOidcProviderConfig(@NonNull String providerId) + throws FirebaseAuthException { + return getOidcProviderConfigOp(providerId).call(); + } + + /** + * Similar to {@link #getOidcProviderConfig(String)} but performs the operation asynchronously. + * Page size is limited to 100 provider configs. + * + * @param providerId A provider ID string. + * @return An {@code ApiFuture} which will complete successfully with an + * {@link OidcProviderConfig} instance. If an error occurs while retrieving the provider + * config or if the specified provider ID does not exist, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not + * prefixed with 'oidc.'. + */ + public ApiFuture getOidcProviderConfigAsync(@NonNull String providerId) { + return getOidcProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation + getOidcProviderConfigOp(final String providerId) { + checkNotDestroyed(); + OidcProviderConfig.checkOidcProviderId(providerId); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected OidcProviderConfig execute() throws FirebaseAuthException { + return userManager.getOidcProviderConfig(providerId); + } + }; + } + + /** + * Gets a page of OpenID Connect auth provider configs starting from the specified + * {@code pageToken}. Page size is limited to 100 provider configs. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @return A {@link ListProviderConfigsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty + * @throws FirebaseAuthException If an error occurs while retrieving provider config data. + */ + public ListProviderConfigsPage listOidcProviderConfigs( + @Nullable String pageToken) throws FirebaseAuthException { + int maxResults = FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS; + return listOidcProviderConfigsOp(pageToken, maxResults).call(); + } + + /** + * Gets a page of OpenID Connect auth provider configs starting from the specified + * {@code pageToken}. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @param maxResults Maximum number of provider configs to include in the returned page. This may + * not exceed 100. + * @return A {@link ListProviderConfigsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + * @throws FirebaseAuthException If an error occurs while retrieving provider config data. + */ + public ListProviderConfigsPage listOidcProviderConfigs( + @Nullable String pageToken, int maxResults) throws FirebaseAuthException { + return listOidcProviderConfigsOp(pageToken, maxResults).call(); + } + + /** + * Similar to {@link #listOidcProviderConfigs(String)} but performs the operation asynchronously. + * Page size is limited to 100 provider configs. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @return An {@code ApiFuture} which will complete successfully with a + * {@link ListProviderConfigsPage} instance. If an error occurs while retrieving provider + * config data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty. + */ + public ApiFuture> listOidcProviderConfigsAsync( + @Nullable String pageToken) { + int maxResults = FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS; + return listOidcProviderConfigsAsync(pageToken, maxResults); + } + + /** + * Similar to {@link #listOidcProviderConfigs(String, int)} but performs the operation + * asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @param maxResults Maximum number of provider configs to include in the returned page. This may + * not exceed 100. + * @return An {@code ApiFuture} which will complete successfully with a + * {@link ListProviderConfigsPage} instance. If an error occurs while retrieving provider + * config data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + */ + public ApiFuture> listOidcProviderConfigsAsync( + @Nullable String pageToken, + int maxResults) { + return listOidcProviderConfigsOp(pageToken, maxResults).callAsync(firebaseApp); + } + + private CallableOperation, FirebaseAuthException> + listOidcProviderConfigsOp(@Nullable final String pageToken, final int maxResults) { + checkNotDestroyed(); + final FirebaseUserManager userManager = getUserManager(); + final DefaultOidcProviderConfigSource source = new DefaultOidcProviderConfigSource(userManager); + final ListProviderConfigsPage.Factory factory = + new ListProviderConfigsPage.Factory(source, maxResults, pageToken); + return + new CallableOperation, FirebaseAuthException>() { + @Override + protected ListProviderConfigsPage execute() + throws FirebaseAuthException { + return factory.create(); + } + }; + } + + /** + * Deletes the OpenID Connect auth provider config identified by the specified provider ID. + * + * @param providerId A provider ID string. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with 'oidc'. + * @throws FirebaseAuthException If an error occurs while deleting the provider config. + */ + public void deleteOidcProviderConfig(@NonNull String providerId) throws FirebaseAuthException { + deleteOidcProviderConfigOp(providerId).call(); + } + + /** + * Similar to {@link #deleteOidcProviderConfig} but performs the operation asynchronously. + * + * @param providerId A provider ID string. + * @return An {@code ApiFuture} which will complete successfully when the specified provider + * config has been deleted. If an error occurs while deleting the provider config, the future + * throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with "oidc.". + */ + public ApiFuture deleteOidcProviderConfigAsync(String providerId) { + return deleteOidcProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation deleteOidcProviderConfigOp( + final String providerId) { + checkNotDestroyed(); + OidcProviderConfig.checkOidcProviderId(providerId); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + userManager.deleteOidcProviderConfig(providerId); + return null; + } + }; + } + + /** + * Creates a new SAML Auth provider config with the attributes contained in the specified + * {@link SamlProviderConfig.CreateRequest}. + * + * @param request A non-null {@link SamlProviderConfig.CreateRequest} instance. + * @return An {@link SamlProviderConfig} instance corresponding to the newly created provider + * config. + * @throws NullPointerException if the provided request is null. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with 'saml'. + * @throws FirebaseAuthException if an error occurs while creating the provider config. + */ + public SamlProviderConfig createSamlProviderConfig( + @NonNull SamlProviderConfig.CreateRequest request) throws FirebaseAuthException { + return createSamlProviderConfigOp(request).call(); + } + + /** + * Similar to {@link #createSamlProviderConfig} but performs the operation asynchronously. + * + * @param request A non-null {@link SamlProviderConfig.CreateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link SamlProviderConfig} + * instance corresponding to the newly created provider config. If an error occurs while + * creating the provider config, the future throws a {@link FirebaseAuthException}. + * @throws NullPointerException if the provided request is null. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with 'saml'. + */ + public ApiFuture createSamlProviderConfigAsync( + @NonNull SamlProviderConfig.CreateRequest request) { + return createSamlProviderConfigOp(request).callAsync(firebaseApp); + } + + private CallableOperation + createSamlProviderConfigOp(final SamlProviderConfig.CreateRequest request) { + checkNotDestroyed(); + checkNotNull(request, "Create request must not be null."); + SamlProviderConfig.checkSamlProviderId(request.getProviderId()); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected SamlProviderConfig execute() throws FirebaseAuthException { + return userManager.createSamlProviderConfig(request); + } + }; + } + + /** + * Updates an existing SAML Auth provider config with the attributes contained in the specified + * {@link SamlProviderConfig.UpdateRequest}. + * + * @param request A non-null {@link SamlProviderConfig.UpdateRequest} instance. + * @return A {@link SamlProviderConfig} instance corresponding to the updated provider config. + * @throws NullPointerException if the provided update request is null. + * @throws IllegalArgumentException If the provided update request is invalid. + * @throws FirebaseAuthException if an error occurs while updating the provider config. + */ + public SamlProviderConfig updateSamlProviderConfig( + @NonNull SamlProviderConfig.UpdateRequest request) throws FirebaseAuthException { + return updateSamlProviderConfigOp(request).call(); + } + + /** + * Similar to {@link #updateSamlProviderConfig} but performs the operation asynchronously. + * + * @param request A non-null {@link SamlProviderConfig.UpdateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link SamlProviderConfig} + * instance corresponding to the updated provider config. If an error occurs while updating + * the provider config, the future throws a {@link FirebaseAuthException}. + * @throws NullPointerException if the provided update request is null. + * @throws IllegalArgumentException If the provided update request is invalid. + */ + public ApiFuture updateSamlProviderConfigAsync( + @NonNull SamlProviderConfig.UpdateRequest request) { + return updateSamlProviderConfigOp(request).callAsync(firebaseApp); + } + + private CallableOperation updateSamlProviderConfigOp( + final SamlProviderConfig.UpdateRequest request) { + checkNotDestroyed(); + checkNotNull(request, "Update request must not be null."); + checkArgument(!request.getProperties().isEmpty(), + "Update request must have at least one property set."); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected SamlProviderConfig execute() throws FirebaseAuthException { + return userManager.updateSamlProviderConfig(request); + } + }; + } + + /** + * Gets the SAML Auth provider config corresponding to the specified provider ID. + * + * @param providerId A provider ID string. + * @return An {@link SamlProviderConfig} instance. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with 'saml'. + * @throws FirebaseAuthException If an error occurs while retrieving the provider config. + */ + public SamlProviderConfig getSamlProviderConfig(@NonNull String providerId) + throws FirebaseAuthException { + return getSamlProviderConfigOp(providerId).call(); + } + + /** + * Similar to {@link #getSamlProviderConfig(String)} but performs the operation asynchronously. + * Page size is limited to 100 provider configs. + * + * @param providerId A provider ID string. + * @return An {@code ApiFuture} which will complete successfully with an + * {@link SamlProviderConfig} instance. If an error occurs while retrieving the provider + * config or if the specified provider ID does not exist, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with 'saml'. + */ + public ApiFuture getSamlProviderConfigAsync(@NonNull String providerId) { + return getSamlProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation + getSamlProviderConfigOp(final String providerId) { + checkNotDestroyed(); + SamlProviderConfig.checkSamlProviderId(providerId); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected SamlProviderConfig execute() throws FirebaseAuthException { + return userManager.getSamlProviderConfig(providerId); + } + }; + } + + /** + * Gets a page of SAML Auth provider configs starting from the specified {@code pageToken}. Page + * size is limited to 100 provider configs. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @return A {@link ListProviderConfigsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty. + * @throws FirebaseAuthException If an error occurs while retrieving provider config data. + */ + public ListProviderConfigsPage listSamlProviderConfigs( + @Nullable String pageToken) throws FirebaseAuthException { + return listSamlProviderConfigs( + pageToken, + FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS); + } + + /** + * Gets a page of SAML Auth provider configs starting from the specified {@code pageToken}. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @param maxResults Maximum number of provider configs to include in the returned page. This may + * not exceed 100. + * @return A {@link ListProviderConfigsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + * @throws FirebaseAuthException If an error occurs while retrieving provider config data. + */ + public ListProviderConfigsPage listSamlProviderConfigs( + @Nullable String pageToken, int maxResults) throws FirebaseAuthException { + return listSamlProviderConfigsOp(pageToken, maxResults).call(); + } + + /** + * Similar to {@link #listSamlProviderConfigs(String)} but performs the operation asynchronously. + * Page size is limited to 100 provider configs. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @return An {@code ApiFuture} which will complete successfully with a + * {@link ListProviderConfigsPage} instance. If an error occurs while retrieving provider + * config data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty. + */ + public ApiFuture> listSamlProviderConfigsAsync( + @Nullable String pageToken) { + int maxResults = FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS; + return listSamlProviderConfigsAsync(pageToken, maxResults); + } + + /** + * Similar to {@link #listSamlProviderConfigs(String, int)} but performs the operation + * asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @param maxResults Maximum number of provider configs to include in the returned page. This may + * not exceed 100. + * @return An {@code ApiFuture} which will complete successfully with a + * {@link ListProviderConfigsPage} instance. If an error occurs while retrieving provider + * config data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + */ + public ApiFuture> listSamlProviderConfigsAsync( + @Nullable String pageToken, + int maxResults) { + return listSamlProviderConfigsOp(pageToken, maxResults).callAsync(firebaseApp); + } + + private CallableOperation, FirebaseAuthException> + listSamlProviderConfigsOp(@Nullable final String pageToken, final int maxResults) { + checkNotDestroyed(); + final FirebaseUserManager userManager = getUserManager(); + final DefaultSamlProviderConfigSource source = new DefaultSamlProviderConfigSource(userManager); + final ListProviderConfigsPage.Factory factory = + new ListProviderConfigsPage.Factory(source, maxResults, pageToken); + return + new CallableOperation, FirebaseAuthException>() { + @Override + protected ListProviderConfigsPage execute() + throws FirebaseAuthException { + return factory.create(); + } + }; + } + + /** + * Deletes the SAML Auth provider config identified by the specified provider ID. + * + * @param providerId A provider ID string. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with "saml.". + * @throws FirebaseAuthException If an error occurs while deleting the provider config. + */ + public void deleteSamlProviderConfig(@NonNull String providerId) throws FirebaseAuthException { + deleteSamlProviderConfigOp(providerId).call(); + } + + /** + * Similar to {@link #deleteSamlProviderConfig} but performs the operation asynchronously. + * + * @param providerId A provider ID string. + * @return An {@code ApiFuture} which will complete successfully when the specified provider + * config has been deleted. If an error occurs while deleting the provider config, the future + * throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with "saml.". + */ + public ApiFuture deleteSamlProviderConfigAsync(String providerId) { + return deleteSamlProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation deleteSamlProviderConfigOp( + final String providerId) { + checkNotDestroyed(); + SamlProviderConfig.checkSamlProviderId(providerId); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + userManager.deleteSamlProviderConfig(providerId); + return null; + } + }; + } + + FirebaseApp getFirebaseApp() { + return this.firebaseApp; + } + + FirebaseTokenVerifier getCookieVerifier() { + return this.cookieVerifier.get(); + } + + FirebaseUserManager getUserManager() { + return this.userManager.get(); + } + + protected Supplier threadSafeMemoize(final Supplier supplier) { + return Suppliers.memoize( + new Supplier() { + @Override + public T get() { + checkNotNull(supplier); + synchronized (lock) { + checkNotDestroyed(); + return supplier.get(); + } + } + }); + } + + void checkNotDestroyed() { + synchronized (lock) { + checkState( + !destroyed.get(), + "FirebaseAuth instance is no longer alive. This happens when " + + "the parent FirebaseApp instance has been deleted."); + } + } + + final void destroy() { + synchronized (lock) { + doDestroy(); + destroyed.set(true); + } + } + + /** Performs any additional required clean up. */ + protected abstract void doDestroy(); + + static Builder builder() { + return new Builder(); + } + + static class Builder { + protected FirebaseApp firebaseApp; + private Supplier tokenFactory; + private Supplier idTokenVerifier; + private Supplier cookieVerifier; + private Supplier userManager; + + private Builder() {} + + Builder setFirebaseApp(FirebaseApp firebaseApp) { + this.firebaseApp = firebaseApp; + return this; + } + + Builder setTokenFactory(Supplier tokenFactory) { + this.tokenFactory = tokenFactory; + return this; + } + + Builder setIdTokenVerifier(Supplier idTokenVerifier) { + this.idTokenVerifier = idTokenVerifier; + return this; + } + + Builder setCookieVerifier(Supplier cookieVerifier) { + this.cookieVerifier = cookieVerifier; + return this; + } + + Builder setUserManager(Supplier userManager) { + this.userManager = userManager; + return this; + } + } +} diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 923778af4..f44bd2343 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,36 +18,19 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import com.google.api.client.json.JsonFactory; import com.google.api.client.util.Clock; import com.google.api.core.ApiFuture; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import com.google.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; -import com.google.firebase.auth.UserRecord.CreateRequest; -import com.google.firebase.auth.UserRecord.UpdateRequest; import com.google.firebase.auth.internal.FirebaseTokenFactory; +import com.google.firebase.auth.multitenancy.TenantManager; import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; -import com.google.firebase.internal.Nullable; - -import java.io.IOException; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; /** * This class is the entry point for all server-side Firebase Authentication actions. @@ -57,29 +40,24 @@ * custom tokens for use by client-side code, verifying Firebase ID Tokens received from clients, or * creating new FirebaseApp instances that are scoped to a particular authentication UID. */ -public class FirebaseAuth { +public final class FirebaseAuth extends AbstractFirebaseAuth { private static final String SERVICE_ID = FirebaseAuth.class.getName(); - private static final String ERROR_CUSTOM_TOKEN = "ERROR_CUSTOM_TOKEN"; - - private final Object lock = new Object(); - private final AtomicBoolean destroyed = new AtomicBoolean(false); + private final Supplier tenantManager; - private final FirebaseApp firebaseApp; - private final Supplier tokenFactory; - private final Supplier idTokenVerifier; - private final Supplier cookieVerifier; - private final Supplier userManager; - private final JsonFactory jsonFactory; + FirebaseAuth(final Builder builder) { + super(builder); + tenantManager = threadSafeMemoize(new Supplier() { + @Override + public TenantManager get() { + return new TenantManager(builder.firebaseApp); + } + }); + } - private FirebaseAuth(Builder builder) { - this.firebaseApp = checkNotNull(builder.firebaseApp); - this.tokenFactory = threadSafeMemoize(builder.tokenFactory); - this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier); - this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier); - this.userManager = threadSafeMemoize(builder.userManager); - this.jsonFactory = firebaseApp.getOptions().getJsonFactory(); + public TenantManager getTenantManager() { + return tenantManager.get(); } /** @@ -98,8 +76,8 @@ public static FirebaseAuth getInstance() { * @return A FirebaseAuth instance. */ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { - FirebaseAuthService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID, - FirebaseAuthService.class); + FirebaseAuthService service = + ImplFirebaseTrampolines.getService(app, SERVICE_ID, FirebaseAuthService.class); if (service == null) { service = ImplFirebaseTrampolines.addService(app, new FirebaseAuthService(app)); } @@ -107,8 +85,8 @@ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { } /** - * Creates a new Firebase session cookie from the given ID token and options. The returned JWT - * can be set as a server-side session cookie with a custom cookie policy. + * Creates a new Firebase session cookie from the given ID token and options. The returned JWT can + * be set as a server-side session cookie with a custom cookie policy. * * @param idToken The Firebase ID token to exchange for a session cookie. * @param options Additional options required to create the cookie. @@ -116,8 +94,8 @@ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. * @throws FirebaseAuthException If an error occurs while generating the session cookie. */ - public String createSessionCookie( - @NonNull String idToken, @NonNull SessionCookieOptions options) throws FirebaseAuthException { + public String createSessionCookie(@NonNull String idToken, @NonNull SessionCookieOptions options) + throws FirebaseAuthException { return createSessionCookieOp(idToken, options).call(); } @@ -127,14 +105,14 @@ public String createSessionCookie( * * @param idToken The Firebase ID token to exchange for a session cookie. * @param options Additional options required to create the cookie. - * @return An {@code ApiFuture} which will complete successfully with a session cookie string. - * If an error occurs while generating the cookie or if the specified ID token is invalid, - * the future throws a {@link FirebaseAuthException}. + * @return An {@code ApiFuture} which will complete successfully with a session cookie string. If + * an error occurs while generating the cookie or if the specified ID token is invalid, the + * future throws a {@link FirebaseAuthException}. * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. */ public ApiFuture createSessionCookieAsync( @NonNull String idToken, @NonNull SessionCookieOptions options) { - return createSessionCookieOp(idToken, options).callAsync(firebaseApp); + return createSessionCookieOp(idToken, options).callAsync(getFirebaseApp()); } private CallableOperation createSessionCookieOp( @@ -157,8 +135,8 @@ protected String execute() throws FirebaseAuthException { *

      If verified successfully, returns a parsed version of the cookie from which the UID and the * other claims can be read. If the cookie is invalid, throws a {@link FirebaseAuthException}. * - *

      This method does not check whether the cookie has been revoked. See - * {@link #verifySessionCookie(String, boolean)}. + *

      This method does not check whether the cookie has been revoked. See {@link + * #verifySessionCookie(String, boolean)}. * * @param cookie A Firebase session cookie string to verify and parse. * @return A {@link FirebaseToken} representing the verified and decoded cookie. @@ -170,20 +148,18 @@ public FirebaseToken verifySessionCookie(String cookie) throws FirebaseAuthExcep /** * Parses and verifies a Firebase session cookie. * - *

      If {@code checkRevoked} is true, additionally verifies that the cookie has not been - * revoked. + *

      If {@code checkRevoked} is true, additionally verifies that the cookie has not been revoked. * *

      If verified successfully, returns a parsed version of the cookie from which the UID and the - * other claims can be read. If the cookie is invalid or has been revoked while - * {@code checkRevoked} is true, throws a {@link FirebaseAuthException}. + * other claims can be read. If the cookie is invalid or has been revoked while {@code + * checkRevoked} is true, throws a {@link FirebaseAuthException}. * * @param cookie A Firebase session cookie string to verify and parse. - * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly - * revoked. + * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. * @return A {@link FirebaseToken} representing the verified and decoded cookie. */ - public FirebaseToken verifySessionCookie( - String cookie, boolean checkRevoked) throws FirebaseAuthException { + public FirebaseToken verifySessionCookie(String cookie, boolean checkRevoked) + throws FirebaseAuthException { return verifySessionCookieOp(cookie, checkRevoked).call(); } @@ -203,13 +179,12 @@ public ApiFuture verifySessionCookieAsync(String cookie) { * asynchronously. * * @param cookie A Firebase session cookie string to verify and parse. - * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly - * revoked. + * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or * unsuccessfully with the failure Exception. */ public ApiFuture verifySessionCookieAsync(String cookie, boolean checkRevoked) { - return verifySessionCookieOp(cookie, checkRevoked).callAsync(firebaseApp); + return verifySessionCookieOp(cookie, checkRevoked).callAsync(getFirebaseApp()); } private CallableOperation verifySessionCookieOp( @@ -227,7 +202,7 @@ public FirebaseToken execute() throws FirebaseAuthException { @VisibleForTesting FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) { - FirebaseTokenVerifier verifier = cookieVerifier.get(); + FirebaseTokenVerifier verifier = getCookieVerifier(); if (checkRevoked) { FirebaseUserManager userManager = getUserManager(); verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager); @@ -235,1109 +210,41 @@ FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) { return verifier; } - /** - * Creates a Firebase custom token for the given UID. This token can then be sent back to a client - * application to be used with the - * signInWithCustomToken - * authentication API. - * - *

      {@link FirebaseApp} must have been initialized with service account credentials to use - * call this method. - * - * @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. - * @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 FirebaseAuthException If an error occurs while generating the custom token. - */ - public String createCustomToken(@NonNull String uid) throws FirebaseAuthException { - return createCustomToken(uid, null); - } - - /** - * Creates a Firebase custom token for the given UID, containing the specified additional - * claims. This token can then be sent back to a client application to be used with the - * signInWithCustomToken - * authentication API. - * - *

      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. - * @param developerClaims Additional claims to be stored in the token (and made available to - * 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. - * @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, - @Nullable Map developerClaims) throws FirebaseAuthException { - return createCustomTokenOp(uid, developerClaims).call(); - } - - /** - * Similar to {@link #createCustomToken(String)} but performs the operation asynchronously. - * - * @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. - * @return An {@code ApiFuture} which will complete successfully with the created Firebase custom - * token, or unsuccessfully with the failure Exception. - * @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not - * been initialized with service account credentials. - */ - public ApiFuture createCustomTokenAsync(@NonNull String uid) { - return createCustomTokenAsync(uid, null); - } - - /** - * Similar to {@link #createCustomToken(String, Map)} but performs the operation - * asynchronously. - * - * @param uid The UID to store in the token. This identifies the user to other Firebase services - * (Realtime Database, Storage, etc.). Should be less than 128 characters. - * @param developerClaims Additional claims to be stored in the token (and made available to - * 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 An {@code ApiFuture} which will complete successfully with the created Firebase custom - * token, or unsuccessfully with the failure Exception. - * @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not - * been initialized with service account credentials. - */ - public ApiFuture createCustomTokenAsync( - @NonNull String uid, @Nullable Map developerClaims) { - return createCustomTokenOp(uid, developerClaims).callAsync(firebaseApp); - } - - private CallableOperation createCustomTokenOp( - final String uid, final Map developerClaims) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - final FirebaseTokenFactory tokenFactory = this.tokenFactory.get(); - return new CallableOperation() { - @Override - public String execute() throws FirebaseAuthException { - try { - return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims); - } catch (IOException e) { - throw new FirebaseAuthException(ERROR_CUSTOM_TOKEN, - "Failed to generate a custom token", e); - } - } - }; - } - - /** - * Parses and verifies a Firebase ID Token. - * - *

      A Firebase application can identify itself to a trusted backend server by sending its - * Firebase ID Token (accessible via the {@code getToken} API in the Firebase Authentication - * client) with its requests. The backend server can then use the {@code verifyIdToken()} method - * to verify that the token is valid. This method ensures that the token is correctly signed, - * has not expired, and it was issued to the Firebase project associated with this - * {@link FirebaseAuth} instance. - * - *

      This method does not check whether a token has been revoked. Use - * {@link #verifyIdToken(String, boolean)} to perform an additional revocation check. - * - * @param token A Firebase ID token string to parse and verify. - * @return A {@link FirebaseToken} representing the verified and decoded token. - * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} - * instance does not have a project ID associated with it. - * @throws FirebaseAuthException If an error occurs while parsing or validating the token. - */ - public FirebaseToken verifyIdToken(@NonNull String token) throws FirebaseAuthException { - return verifyIdToken(token, false); - } - - /** - * Parses and verifies a Firebase ID Token. - * - *

      A Firebase application can identify itself to a trusted backend server by sending its - * Firebase ID Token (accessible via the {@code getToken} API in the Firebase Authentication - * client) with its requests. The backend server can then use the {@code verifyIdToken()} method - * to verify that the token is valid. This method ensures that the token is correctly signed, - * has not expired, and it was issued to the Firebase project associated with this - * {@link FirebaseAuth} instance. - * - *

      If {@code checkRevoked} is set to true, this method performs an additional check to see - * if the ID token has been revoked since it was issues. This requires making an additional - * remote API call. - * - * @param token A Firebase ID token string to parse and verify. - * @param checkRevoked A boolean denoting whether to check if the tokens were revoked. - * @return A {@link FirebaseToken} representing the verified and decoded token. - * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} - * instance does not have a project ID associated with it. - * @throws FirebaseAuthException If an error occurs while parsing or validating the token. - */ - public FirebaseToken verifyIdToken( - @NonNull String token, boolean checkRevoked) throws FirebaseAuthException { - return verifyIdTokenOp(token, checkRevoked).call(); - } - - /** - * Similar to {@link #verifyIdToken(String)} but performs the operation asynchronously. - * - * @param token A Firebase ID Token to verify and parse. - * @return An {@code ApiFuture} which will complete successfully with the parsed token, or - * unsuccessfully with a {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} - * instance does not have a project ID associated with it. - */ - public ApiFuture verifyIdTokenAsync(@NonNull String token) { - return verifyIdTokenAsync(token, false); - } - - /** - * Similar to {@link #verifyIdToken(String, boolean)} but performs the operation asynchronously. - * - * @param token A Firebase ID Token to verify and parse. - * @param checkRevoked A boolean denoting whether to check if the tokens were revoked. - * @return An {@code ApiFuture} which will complete successfully with the parsed token, or - * unsuccessfully with a {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} - * instance does not have a project ID associated with it. - */ - public ApiFuture verifyIdTokenAsync(@NonNull String token, boolean checkRevoked) { - return verifyIdTokenOp(token, checkRevoked).callAsync(firebaseApp); - } - - private CallableOperation verifyIdTokenOp( - final String token, final boolean checkRevoked) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(token), "ID token must not be null or empty"); - final FirebaseTokenVerifier verifier = getIdTokenVerifier(checkRevoked); - return new CallableOperation() { - @Override - protected FirebaseToken execute() throws FirebaseAuthException { - return verifier.verifyToken(token); - } - }; - } - - @VisibleForTesting - FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) { - FirebaseTokenVerifier verifier = idTokenVerifier.get(); - if (checkRevoked) { - FirebaseUserManager userManager = getUserManager(); - verifier = RevocationCheckDecorator.decorateIdTokenVerifier(verifier, userManager); - } - return verifier; - } - - /** - * Revokes all refresh tokens for the specified user. - * - *

      Updates the user's tokensValidAfterTimestamp to the current UTC time expressed in - * milliseconds since the epoch and truncated to 1 second accuracy. It is important that the - * server on which this is called has its clock set correctly and synchronized. - * - *

      While this will revoke all sessions for a specified user and disable any new ID tokens for - * existing sessions from getting minted, existing ID tokens may remain active until their - * natural expiration (one hour). - * To verify that ID tokens are revoked, use {@link #verifyIdTokenAsync(String, boolean)}. - * - * @param uid The user id for which tokens are revoked. - * @throws IllegalArgumentException If the user ID is null or empty. - * @throws FirebaseAuthException If an error occurs while revoking tokens. - */ - public void revokeRefreshTokens(@NonNull String uid) throws FirebaseAuthException { - revokeRefreshTokensOp(uid).call(); - } - - /** - * Similar to {@link #revokeRefreshTokens(String)} but performs the operation asynchronously. - * - * @param uid The user id for which tokens are revoked. - * @return An {@code ApiFuture} which will complete successfully or fail with a - * {@link FirebaseAuthException} in the event of an error. - * @throws IllegalArgumentException If the user ID is null or empty. - */ - public ApiFuture revokeRefreshTokensAsync(@NonNull String uid) { - return revokeRefreshTokensOp(uid).callAsync(firebaseApp); - } - - private CallableOperation revokeRefreshTokensOp(final String uid) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected Void execute() throws FirebaseAuthException { - int currentTimeSeconds = (int) (System.currentTimeMillis() / 1000); - UpdateRequest request = new UpdateRequest(uid).setValidSince(currentTimeSeconds); - userManager.updateUser(request, jsonFactory); - return null; - } - }; - } - - /** - * Gets the user data corresponding to the specified user ID. - * - * @param uid A user ID string. - * @return A {@link UserRecord} instance. - * @throws IllegalArgumentException If the user ID string is null or empty. - * @throws FirebaseAuthException If an error occurs while retrieving user data. - */ - public UserRecord getUser(@NonNull String uid) throws FirebaseAuthException { - return getUserOp(uid).call(); - } - - /** - * Similar to {@link #getUser(String)} but performs the operation asynchronously. - * - * @param uid A user ID string. - * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} - * instance. If an error occurs while retrieving user data or if the specified user ID does - * not exist, the future throws a {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the user ID string is null or empty. - */ - public ApiFuture getUserAsync(@NonNull String uid) { - return getUserOp(uid).callAsync(firebaseApp); - } - - private CallableOperation getUserOp(final String uid) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected UserRecord execute() throws FirebaseAuthException { - return userManager.getUserById(uid); - } - }; - } - - /** - * Gets the user data corresponding to the specified user email. - * - * @param email A user email address string. - * @return A {@link UserRecord} instance. - * @throws IllegalArgumentException If the email is null or empty. - * @throws FirebaseAuthException If an error occurs while retrieving user data. - */ - public UserRecord getUserByEmail(@NonNull String email) throws FirebaseAuthException { - return getUserByEmailOp(email).call(); - } - - /** - * Similar to {@link #getUserByEmail(String)} but performs the operation asynchronously. - * - * @param email A user email address string. - * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} - * instance. If an error occurs while retrieving user data or if the email address does not - * correspond to a user, the future throws a {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the email is null or empty. - */ - public ApiFuture getUserByEmailAsync(@NonNull String email) { - return getUserByEmailOp(email).callAsync(firebaseApp); - } - - private CallableOperation getUserByEmailOp( - final String email) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected UserRecord execute() throws FirebaseAuthException { - return userManager.getUserByEmail(email); - } - }; - } - - /** - * Gets the user data corresponding to the specified user phone number. - * - * @param phoneNumber A user phone number string. - * @return A a {@link UserRecord} instance. - * @throws IllegalArgumentException If the phone number is null or empty. - * @throws FirebaseAuthException If an error occurs while retrieving user data. - */ - public UserRecord getUserByPhoneNumber(@NonNull String phoneNumber) throws FirebaseAuthException { - return getUserByPhoneNumberOp(phoneNumber).call(); - } - - /** - * Gets the user data corresponding to the specified user phone number. - * - * @param phoneNumber A user phone number string. - * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} - * instance. If an error occurs while retrieving user data or if the phone number does not - * correspond to a user, the future throws a {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the phone number is null or empty. - */ - public ApiFuture getUserByPhoneNumberAsync(@NonNull String phoneNumber) { - return getUserByPhoneNumberOp(phoneNumber).callAsync(firebaseApp); - } - - private CallableOperation getUserByPhoneNumberOp( - final String phoneNumber) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number must not be null or empty"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected UserRecord execute() throws FirebaseAuthException { - return userManager.getUserByPhoneNumber(phoneNumber); - } - }; - } - - /** - * Gets the user data corresponding to the specified identifiers. - * - *

      There are no ordering guarantees; in particular, the nth entry in the users result list is - * not guaranteed to correspond to the nth entry in the input parameters list. - * - *

      A maximum of 100 identifiers may be specified. If more than 100 identifiers are - * supplied, this method throws an {@link IllegalArgumentException}. - * - * @param identifiers The identifiers used to indicate which user records should be returned. Must - * have 100 or fewer entries. - * @return The corresponding user records. - * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 - * identifiers are specified. - * @throws NullPointerException If the identifiers parameter is null. - * @throws FirebaseAuthException If an error occurs while retrieving user data. - */ - public GetUsersResult getUsers(@NonNull Collection identifiers) - throws FirebaseAuthException { - return getUsersOp(identifiers).call(); - } - - /** - * Gets the user data corresponding to the specified identifiers. - * - *

      There are no ordering guarantees; in particular, the nth entry in the users result list is - * not guaranteed to correspond to the nth entry in the input parameters list. - * - *

      A maximum of 100 identifiers may be specified. If more than 100 identifiers are - * supplied, this method throws an {@link IllegalArgumentException}. - * - * @param identifiers The identifiers used to indicate which user records should be returned. - * Must have 100 or fewer entries. - * @return An {@code ApiFuture} that resolves to the corresponding user records. - * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 - * identifiers are specified. - * @throws NullPointerException If the identifiers parameter is null. - */ - public ApiFuture getUsersAsync(@NonNull Collection identifiers) { - return getUsersOp(identifiers).callAsync(firebaseApp); - } - - private CallableOperation getUsersOp( - @NonNull final Collection identifiers) { - checkNotDestroyed(); - checkNotNull(identifiers, "identifiers must not be null"); - checkArgument(identifiers.size() <= FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE, - "identifiers parameter must have <= " + FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE - + " entries."); - - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected GetUsersResult execute() throws FirebaseAuthException { - Set users = userManager.getAccountInfo(identifiers); - Set notFound = new HashSet<>(); - for (UserIdentifier id : identifiers) { - if (!isUserFound(id, users)) { - notFound.add(id); - } - } - return new GetUsersResult(users, notFound); - } - }; - } - - private boolean isUserFound(UserIdentifier id, Collection userRecords) { - for (UserRecord userRecord : userRecords) { - if (id.matches(userRecord)) { - return true; - } - } - return false; - } - - /** - * Gets a page of users starting from the specified {@code pageToken}. Page size is - * limited to 1000 users. - * - * @param pageToken A non-empty page token string, or null to retrieve the first page of users. - * @return A {@link ListUsersPage} instance. - * @throws IllegalArgumentException If the specified page token is empty. - * @throws FirebaseAuthException If an error occurs while retrieving user data. - */ - public ListUsersPage listUsers(@Nullable String pageToken) throws FirebaseAuthException { - return listUsers(pageToken, FirebaseUserManager.MAX_LIST_USERS_RESULTS); - } - - /** - * Gets a page of users starting from the specified {@code pageToken}. - * - * @param pageToken A non-empty page token string, or null to retrieve the first page of users. - * @param maxResults Maximum number of users to include in the returned page. This may not - * exceed 1000. - * @return A {@link ListUsersPage} instance. - * @throws IllegalArgumentException If the specified page token is empty, or max results value - * is invalid. - * @throws FirebaseAuthException If an error occurs while retrieving user data. - */ - public ListUsersPage listUsers( - @Nullable String pageToken, int maxResults) throws FirebaseAuthException { - return listUsersOp(pageToken, maxResults).call(); - } - - /** - * Similar to {@link #listUsers(String)} but performs the operation asynchronously. - * - * @param pageToken A non-empty page token string, or null to retrieve the first page of users. - * @return An {@code ApiFuture} which will complete successfully with a {@link ListUsersPage} - * instance. If an error occurs while retrieving user data, the future throws an exception. - * @throws IllegalArgumentException If the specified page token is empty. - */ - public ApiFuture listUsersAsync(@Nullable String pageToken) { - return listUsersAsync(pageToken, FirebaseUserManager.MAX_LIST_USERS_RESULTS); - } - - /** - * Similar to {@link #listUsers(String, int)} but performs the operation asynchronously. - * - * @param pageToken A non-empty page token string, or null to retrieve the first page of users. - * @param maxResults Maximum number of users to include in the returned page. This may not - * exceed 1000. - * @return An {@code ApiFuture} which will complete successfully with a {@link ListUsersPage} - * instance. If an error occurs while retrieving user data, the future throws an exception. - * @throws IllegalArgumentException If the specified page token is empty, or max results value - * is invalid. - */ - public ApiFuture listUsersAsync(@Nullable String pageToken, int maxResults) { - return listUsersOp(pageToken, maxResults).callAsync(firebaseApp); - } - - private CallableOperation listUsersOp( - @Nullable final String pageToken, final int maxResults) { - checkNotDestroyed(); - final FirebaseUserManager userManager = getUserManager(); - final PageFactory factory = new PageFactory( - new DefaultUserSource(userManager, jsonFactory), maxResults, pageToken); - return new CallableOperation() { - @Override - protected ListUsersPage execute() throws FirebaseAuthException { - return factory.create(); - } - }; - } - - /** - * Creates a new user account with the attributes contained in the specified - * {@link CreateRequest}. - * - * @param request A non-null {@link CreateRequest} instance. - * @return A {@link UserRecord} instance corresponding to the newly created account. - * @throws NullPointerException if the provided request is null. - * @throws FirebaseAuthException if an error occurs while creating the user account. - */ - public UserRecord createUser(@NonNull CreateRequest request) throws FirebaseAuthException { - return createUserOp(request).call(); - } - - /** - * Similar to {@link #createUser(CreateRequest)} but performs the operation asynchronously. - * - * @param request A non-null {@link CreateRequest} instance. - * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} - * instance corresponding to the newly created account. If an error occurs while creating the - * user account, the future throws a {@link FirebaseAuthException}. - * @throws NullPointerException if the provided request is null. - */ - public ApiFuture createUserAsync(@NonNull CreateRequest request) { - return createUserOp(request).callAsync(firebaseApp); - } - - private CallableOperation createUserOp( - final CreateRequest request) { - checkNotDestroyed(); - checkNotNull(request, "create request must not be null"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected UserRecord execute() throws FirebaseAuthException { - String uid = userManager.createUser(request); - return userManager.getUserById(uid); - } - }; - } - - /** - * Updates an existing user account with the attributes contained in the specified - * {@link UpdateRequest}. - * - * @param request A non-null {@link UpdateRequest} instance. - * @return A {@link UserRecord} instance corresponding to the updated user account. - * account, the task fails with a {@link FirebaseAuthException}. - * @throws NullPointerException if the provided update request is null. - * @throws FirebaseAuthException if an error occurs while updating the user account. - */ - public UserRecord updateUser(@NonNull UpdateRequest request) throws FirebaseAuthException { - return updateUserOp(request).call(); - } - - /** - * Similar to {@link #updateUser(UpdateRequest)} but performs the operation asynchronously. - * - * @param request A non-null {@link UpdateRequest} instance. - * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} - * instance corresponding to the updated user account. If an error occurs while updating the - * user account, the future throws a {@link FirebaseAuthException}. - */ - public ApiFuture updateUserAsync(@NonNull UpdateRequest request) { - return updateUserOp(request).callAsync(firebaseApp); - } - - private CallableOperation updateUserOp( - final UpdateRequest request) { - checkNotDestroyed(); - checkNotNull(request, "update request must not be null"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected UserRecord execute() throws FirebaseAuthException { - userManager.updateUser(request, jsonFactory); - return userManager.getUserById(request.getUid()); - } - }; - } - - /** - * Sets the specified custom claims on an existing user account. A null claims value removes - * any claims currently set on the user account. The claims should serialize into a valid JSON - * string. The serialized claims must not be larger than 1000 characters. - * - * @param uid A user ID string. - * @param claims A map of custom claims or null. - * @throws FirebaseAuthException If an error occurs while updating custom claims. - * @throws IllegalArgumentException If the user ID string is null or empty, or the claims - * payload is invalid or too large. - */ - public void setCustomUserClaims(@NonNull String uid, - @Nullable Map claims) throws FirebaseAuthException { - setCustomUserClaimsOp(uid, claims).call(); - } - - /** - * @deprecated Use {@link #setCustomUserClaims(String, Map)} instead. - */ - public void setCustomClaims(@NonNull String uid, - @Nullable Map claims) throws FirebaseAuthException { - setCustomUserClaims(uid, claims); - } - - /** - * Similar to {@link #setCustomUserClaims(String, Map)} but performs the operation asynchronously. - * - * @param uid A user ID string. - * @param claims A map of custom claims or null. - * @return An {@code ApiFuture} which will complete successfully when the user account has been - * updated. If an error occurs while deleting the user account, the future throws a - * {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the user ID string is null or empty. - */ - public ApiFuture setCustomUserClaimsAsync( - @NonNull String uid, @Nullable Map claims) { - return setCustomUserClaimsOp(uid, claims).callAsync(firebaseApp); - } - - private CallableOperation setCustomUserClaimsOp( - final String uid, final Map claims) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected Void execute() throws FirebaseAuthException { - final UpdateRequest request = new UpdateRequest(uid).setCustomClaims(claims); - userManager.updateUser(request, jsonFactory); - return null; - } - }; - } - - /** - * Deletes the user identified by the specified user ID. - * - * @param uid A user ID string. - * @throws IllegalArgumentException If the user ID string is null or empty. - * @throws FirebaseAuthException If an error occurs while deleting the user. - */ - public void deleteUser(@NonNull String uid) throws FirebaseAuthException { - deleteUserOp(uid).call(); - } - - /** - * Similar to {@link #deleteUser(String)} but performs the operation asynchronously. - * - * @param uid A user ID string. - * @return An {@code ApiFuture} which will complete successfully when the specified user account - * has been deleted. If an error occurs while deleting the user account, the future throws a - * {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the user ID string is null or empty. - */ - public ApiFuture deleteUserAsync(String uid) { - return deleteUserOp(uid).callAsync(firebaseApp); - } - - private CallableOperation deleteUserOp(final String uid) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected Void execute() throws FirebaseAuthException { - userManager.deleteUser(uid); - return null; - } - }; - } - - /** - * Deletes the users specified by the given identifiers. - * - *

      Deleting a non-existing user does not generate an error (the method is idempotent). - * Non-existing users are considered to be successfully deleted and are therefore included in the - * DeleteUsersResult.getSuccessCount() value. - * - *

      A maximum of 1000 identifiers may be supplied. If more than 1000 identifiers are - * supplied, this method throws an {@link IllegalArgumentException}. - * - *

      This API has a rate limit of 1 QPS. Exceeding the limit may result in a quota exceeded - * error. If you want to delete more than 1000 users, we suggest adding a delay to ensure you - * don't exceed this limit. - * - * @param uids The uids of the users to be deleted. Must have <= 1000 entries. - * @return The total number of successful/failed deletions, as well as the array of errors that - * correspond to the failed deletions. - * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 - * identifiers are specified. - * @throws FirebaseAuthException If an error occurs while deleting users. - */ - public DeleteUsersResult deleteUsers(List uids) throws FirebaseAuthException { - return deleteUsersOp(uids).call(); - } - - /** - * Similar to {@link #deleteUsers(List)} but performs the operation asynchronously. - * - * @param uids The uids of the users to be deleted. Must have <= 1000 entries. - * @return An {@code ApiFuture} that resolves to the total number of successful/failed - * deletions, as well as the array of errors that correspond to the failed deletions. If an - * error occurs while deleting the user account, the future throws a - * {@link FirebaseAuthException}. - * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 - * identifiers are specified. - */ - public ApiFuture deleteUsersAsync(List uids) { - return deleteUsersOp(uids).callAsync(firebaseApp); - } - - private CallableOperation deleteUsersOp( - final List uids) { - checkNotDestroyed(); - checkNotNull(uids, "uids must not be null"); - for (String uid : uids) { - UserRecord.checkUid(uid); - } - checkArgument(uids.size() <= FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE, - "uids parameter must have <= " + FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE - + " entries."); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected DeleteUsersResult execute() throws FirebaseAuthException { - return userManager.deleteUsers(uids); - } - }; - } - - /** - * Imports the provided list of users into Firebase Auth. You can import a maximum of 1000 users - * at a time. This operation is optimized for bulk imports and does not check identifier - * uniqueness which could result in duplications. - * - *

      {@link UserImportOptions} is required to import users with passwords. See - * {@link #importUsers(List, UserImportOptions)}. - * - * @param users A non-empty list of users to be imported. Length must not exceed 1000. - * @return A {@link UserImportResult} instance. - * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 - * elements. Or if at least one user specifies a password. - * @throws FirebaseAuthException If an error occurs while importing users. - */ - public UserImportResult importUsers(List users) throws FirebaseAuthException { - return importUsers(users, null); - } - - /** - * Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a - * time. This operation is optimized for bulk imports and will ignore checks on identifier - * uniqueness which could result in duplications. - * - * @param users A non-empty list of users to be imported. Length must not exceed 1000. - * @param options a {@link UserImportOptions} instance or null. Required when importing users - * with passwords. - * @return A {@link UserImportResult} instance. - * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 - * elements. Or if at least one user specifies a password, and options is null. - * @throws FirebaseAuthException If an error occurs while importing users. - */ - public UserImportResult importUsers(List users, - @Nullable UserImportOptions options) throws FirebaseAuthException { - return importUsersOp(users, options).call(); - } - - /** - * Similar to {@link #importUsers(List)} but performs the operation asynchronously. - * - * @param users A non-empty list of users to be imported. Length must not exceed 1000. - * @return An {@code ApiFuture} which will complete successfully when the user accounts are - * imported. If an error occurs while importing the users, the future throws a - * {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 - * elements. Or if at least one user specifies a password. - */ - public ApiFuture importUsersAsync(List users) { - return importUsersAsync(users, null); - } - - /** - * Similar to {@link #importUsers(List, UserImportOptions)} but performs the operation - * asynchronously. - * - * @param users A non-empty list of users to be imported. Length must not exceed 1000. - * @param options a {@link UserImportOptions} instance or null. Required when importing users - * with passwords. - * @return An {@code ApiFuture} which will complete successfully when the user accounts are - * imported. If an error occurs while importing the users, the future throws a - * {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 - * elements. Or if at least one user specifies a password, and options is null. - */ - public ApiFuture importUsersAsync(List users, - @Nullable UserImportOptions options) { - return importUsersOp(users, options).callAsync(firebaseApp); - } - - private CallableOperation importUsersOp( - final List users, final UserImportOptions options) { - checkNotDestroyed(); - final UserImportRequest request = new UserImportRequest(users, options, jsonFactory); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected UserImportResult execute() throws FirebaseAuthException { - return userManager.importUsers(request); - } - }; - } - - /** - * 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 object 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. - * @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 object 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. - * @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 object 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. - * @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 object 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. - * @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 object 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. - * @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); - } - - @VisibleForTesting - FirebaseUserManager getUserManager() { - return this.userManager.get(); - } - - 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"); - } - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected String execute() throws FirebaseAuthException { - return userManager.getEmailActionLink(type, email, settings); - } - }; - } - - private Supplier threadSafeMemoize(final Supplier supplier) { - return Suppliers.memoize(new Supplier() { - @Override - public T get() { - checkNotNull(supplier); - synchronized (lock) { - checkNotDestroyed(); - return supplier.get(); - } - } - }); - } - - private void checkNotDestroyed() { - synchronized (lock) { - checkState(!destroyed.get(), "FirebaseAuth instance is no longer alive. This happens when " - + "the parent FirebaseApp instance has been deleted."); - } - } - - private void destroy() { - synchronized (lock) { - destroyed.set(true); - } - } + @Override + protected void doDestroy() { } private static FirebaseAuth fromApp(final FirebaseApp app) { - return FirebaseAuth.builder() - .setFirebaseApp(app) - .setTokenFactory(new Supplier() { - @Override - public FirebaseTokenFactory get() { - return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM); - } - }) - .setIdTokenVerifier(new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM); - } - }) - .setCookieVerifier(new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); - } - }) - .setUserManager(new Supplier() { - @Override - public FirebaseUserManager get() { - return new FirebaseUserManager(app); - } - }) - .build(); - } - - @VisibleForTesting - static Builder builder() { - return new Builder(); - } - - static class Builder { - private FirebaseApp firebaseApp; - private Supplier tokenFactory; - private Supplier idTokenVerifier; - private Supplier cookieVerifier; - private Supplier userManager; - - private Builder() { } - - Builder setFirebaseApp(FirebaseApp firebaseApp) { - this.firebaseApp = firebaseApp; - return this; - } - - Builder setTokenFactory(Supplier tokenFactory) { - this.tokenFactory = tokenFactory; - return this; - } - - Builder setIdTokenVerifier(Supplier idTokenVerifier) { - this.idTokenVerifier = idTokenVerifier; - return this; - } - - Builder setCookieVerifier(Supplier cookieVerifier) { - this.cookieVerifier = cookieVerifier; - return this; - } - - Builder setUserManager(Supplier userManager) { - this.userManager = userManager; - return this; - } - - FirebaseAuth build() { - return new FirebaseAuth(this); - } + return new FirebaseAuth( + AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setTokenFactory( + new Supplier() { + @Override + public FirebaseTokenFactory get() { + return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM); + } + }) + .setIdTokenVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM); + } + }) + .setCookieVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); + } + }) + .setUserManager( + new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager.builder().setFirebaseApp(app).build(); + } + })); } private static class FirebaseAuthService extends FirebaseService { diff --git a/src/main/java/com/google/firebase/auth/FirebaseToken.java b/src/main/java/com/google/firebase/auth/FirebaseToken.java index 3d7b0b254..835fa41c9 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseToken.java +++ b/src/main/java/com/google/firebase/auth/FirebaseToken.java @@ -42,6 +42,15 @@ public String getUid() { return (String) claims.get("sub"); } + /** Returns the tenant ID for the this token. */ + public String getTenantId() { + Map firebase = (Map) claims.get("firebase"); + if (firebase == null) { + return null; + } + return (String) firebase.get("tenant"); + } + /** Returns the Issuer for the this token. */ public String getIssuer() { return (String) claims.get("iss"); @@ -57,14 +66,14 @@ public String getPicture() { return (String) claims.get("picture"); } - /** + /** * Returns the e-mail address for this user, or {@code null} if it's unavailable. */ public String getEmail() { return (String) claims.get("email"); } - /** + /** * Indicates if the email address returned by {@link #getEmail()} has been verified as good. */ public boolean isEmailVerified() { diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java index dbb562872..e0105e9aa 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java @@ -30,6 +30,7 @@ import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.auth.internal.CryptoSigners; import com.google.firebase.auth.internal.FirebaseTokenFactory; +import com.google.firebase.internal.Nullable; import java.io.IOException; @@ -52,11 +53,17 @@ final class FirebaseTokenUtils { private FirebaseTokenUtils() { } static FirebaseTokenFactory createTokenFactory(FirebaseApp firebaseApp, Clock clock) { + return createTokenFactory(firebaseApp, clock, null); + } + + static FirebaseTokenFactory createTokenFactory( + FirebaseApp firebaseApp, Clock clock, @Nullable String tenantId) { try { return new FirebaseTokenFactory( firebaseApp.getOptions().getJsonFactory(), clock, - CryptoSigners.getCryptoSigner(firebaseApp)); + CryptoSigners.getCryptoSigner(firebaseApp), + tenantId); } catch (IOException e) { throw new IllegalStateException( "Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK " @@ -68,6 +75,11 @@ static FirebaseTokenFactory createTokenFactory(FirebaseApp firebaseApp, Clock cl } static FirebaseTokenVerifierImpl createIdTokenVerifier(FirebaseApp app, Clock clock) { + return createIdTokenVerifier(app, clock, null); + } + + static FirebaseTokenVerifierImpl createIdTokenVerifier( + FirebaseApp app, Clock clock, @Nullable String tenantId) { String projectId = ImplFirebaseTrampolines.getProjectId(app); checkState(!Strings.isNullOrEmpty(projectId), "Must initialize FirebaseApp with a project ID to call verifyIdToken()"); @@ -82,6 +94,7 @@ static FirebaseTokenVerifierImpl createIdTokenVerifier(FirebaseApp app, Clock cl .setJsonFactory(app.getOptions().getJsonFactory()) .setPublicKeysManager(publicKeysManager) .setIdTokenVerifier(idTokenVerifier) + .setTenantId(tenantId) .build(); } diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java index c164173a6..e1a5a9a19 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java @@ -28,6 +28,7 @@ import com.google.api.client.util.ArrayMap; import com.google.common.base.Joiner; import com.google.common.base.Strings; +import com.google.firebase.internal.Nullable; import java.io.IOException; import java.math.BigDecimal; import java.security.GeneralSecurityException; @@ -45,6 +46,7 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier { "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"; private static final String ERROR_INVALID_CREDENTIAL = "ERROR_INVALID_CREDENTIAL"; private static final String ERROR_RUNTIME_EXCEPTION = "ERROR_RUNTIME_EXCEPTION"; + static final String TENANT_ID_MISMATCH_ERROR = "tenant-id-mismatch"; private final JsonFactory jsonFactory; private final GooglePublicKeysManager publicKeysManager; @@ -53,6 +55,7 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier { private final String shortName; private final String articledShortName; private final String docUrl; + private final String tenantId; private FirebaseTokenVerifierImpl(Builder builder) { this.jsonFactory = checkNotNull(builder.jsonFactory); @@ -65,6 +68,7 @@ private FirebaseTokenVerifierImpl(Builder builder) { this.shortName = builder.shortName; this.articledShortName = prefixWithIndefiniteArticle(this.shortName); this.docUrl = builder.docUrl; + this.tenantId = Strings.nullToEmpty(builder.tenantId); } /** @@ -90,7 +94,9 @@ public FirebaseToken verifyToken(String token) throws FirebaseAuthException { IdToken idToken = parse(token); checkContents(idToken); checkSignature(idToken); - return new FirebaseToken(idToken.getPayload()); + FirebaseToken firebaseToken = new FirebaseToken(idToken.getPayload()); + checkTenantId(firebaseToken); + return firebaseToken; } GooglePublicKeysManager getPublicKeysManager() { @@ -278,6 +284,18 @@ private boolean containsLegacyUidField(IdToken.Payload payload) { return false; } + private void checkTenantId(final FirebaseToken firebaseToken) throws FirebaseAuthException { + String tokenTenantId = Strings.nullToEmpty(firebaseToken.getTenantId()); + if (!this.tenantId.equals(tokenTenantId)) { + throw new FirebaseAuthException( + TENANT_ID_MISMATCH_ERROR, + String.format( + "The tenant ID ('%s') of the token did not match the expected value ('%s')", + tokenTenantId, + tenantId)); + } + } + static Builder builder() { return new Builder(); } @@ -290,6 +308,7 @@ static final class Builder { private String shortName; private IdTokenVerifier idTokenVerifier; private String docUrl; + private String tenantId; private Builder() { } @@ -323,6 +342,11 @@ Builder setDocUrl(String docUrl) { return this; } + Builder setTenantId(@Nullable String tenantId) { + this.tenantId = tenantId; + return this; + } + FirebaseTokenVerifierImpl build() { return new FirebaseTokenVerifierImpl(this); } diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index ab8759c4f..b73882277 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -20,37 +20,30 @@ 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; -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.GenericJson; import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; import com.google.api.client.util.Key; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; 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.auth.UserRecord.CreateRequest; -import com.google.firebase.auth.UserRecord.UpdateRequest; +import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.auth.internal.BatchDeleteResponse; import com.google.firebase.auth.internal.DownloadAccountResponse; import com.google.firebase.auth.internal.GetAccountInfoRequest; import com.google.firebase.auth.internal.GetAccountInfoResponse; -import com.google.firebase.auth.internal.HttpErrorResponse; +import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse; +import com.google.firebase.auth.internal.ListSamlProviderConfigsResponse; import com.google.firebase.auth.internal.UploadAccountResponse; import com.google.firebase.internal.ApiClientUtils; 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.Collection; import java.util.HashSet; import java.util.List; @@ -66,30 +59,7 @@ */ class FirebaseUserManager { - static final String USER_NOT_FOUND_ERROR = "user-not-found"; - static final String INTERNAL_ERROR = "internal-error"; - - // Map of server-side error codes to SDK error codes. - // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors - private static final Map ERROR_CODES = ImmutableMap.builder() - .put("CLAIMS_TOO_LARGE", "claims-too-large") - .put("CONFIGURATION_NOT_FOUND", "project-not-found") - .put("INSUFFICIENT_PERMISSION", "insufficient-permission") - .put("DUPLICATE_EMAIL", "email-already-exists") - .put("DUPLICATE_LOCAL_ID", "uid-already-exists") - .put("EMAIL_EXISTS", "email-already-exists") - .put("INVALID_CLAIMS", "invalid-claims") - .put("INVALID_EMAIL", "invalid-email") - .put("INVALID_PAGE_SELECTION", "invalid-page-token") - .put("INVALID_PHONE_NUMBER", "invalid-phone-number") - .put("PHONE_NUMBER_EXISTS", "phone-number-already-exists") - .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_PROVIDER_CONFIGS_RESULTS = 100; static final int MAX_GET_ACCOUNTS_BATCH_SIZE = 100; static final int MAX_DELETE_ACCOUNTS_BATCH_SIZE = 1000; static final int MAX_LIST_USERS_RESULTS = 1000; @@ -100,45 +70,41 @@ class FirebaseUserManager { "iss", "jti", "nbf", "nonce", "sub", "firebase"); private static final String ID_TOOLKIT_URL = - "https://identitytoolkit.googleapis.com/v1/projects/%s"; - private static final String CLIENT_VERSION_HEADER = "X-Client-Version"; + "https://identitytoolkit.googleapis.com/%s/projects/%s"; - private final String baseUrl; + private final String userMgtBaseUrl; + private final String idpConfigMgtBaseUrl; private final JsonFactory jsonFactory; - private final HttpRequestFactory requestFactory; - private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); - - private HttpResponseInterceptor interceptor; - - /** - * Creates a new FirebaseUserManager instance. - * - * @param app A non-null {@link FirebaseApp}. - */ - FirebaseUserManager(@NonNull FirebaseApp app) { - this(app, null); - } + private final AuthHttpClient httpClient; - FirebaseUserManager(@NonNull FirebaseApp app, @Nullable HttpRequestFactory requestFactory) { - checkNotNull(app, "FirebaseApp must not be null"); + private FirebaseUserManager(Builder builder) { + FirebaseApp app = checkNotNull(builder.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(); - - if (requestFactory == null) { - requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + final String idToolkitUrlV1 = String.format(ID_TOOLKIT_URL, "v1", projectId); + final String idToolkitUrlV2 = String.format(ID_TOOLKIT_URL, "v2", projectId); + final String tenantId = builder.tenantId; + if (tenantId == null) { + this.userMgtBaseUrl = idToolkitUrlV1; + this.idpConfigMgtBaseUrl = idToolkitUrlV2; + } else { + checkArgument(!tenantId.isEmpty(), "Tenant ID must not be empty."); + this.userMgtBaseUrl = idToolkitUrlV1 + "/tenants/" + tenantId; + this.idpConfigMgtBaseUrl = idToolkitUrlV2 + "/tenants/" + tenantId; } - this.requestFactory = requestFactory; + this.jsonFactory = app.getOptions().getJsonFactory(); + HttpRequestFactory requestFactory = builder.requestFactory == null + ? ApiClientUtils.newAuthorizedRequestFactory(app) : builder.requestFactory; + this.httpClient = new AuthHttpClient(jsonFactory, requestFactory); } @VisibleForTesting void setInterceptor(HttpResponseInterceptor interceptor) { - this.interceptor = interceptor; + httpClient.setInterceptor(interceptor); } UserRecord getUserById(String uid) throws FirebaseAuthException { @@ -147,7 +113,8 @@ UserRecord getUserById(String uid) throws FirebaseAuthException { GetAccountInfoResponse response = post( "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException(USER_NOT_FOUND_ERROR, + throw new FirebaseAuthException( + AuthHttpClient.USER_NOT_FOUND_ERROR, "No user record found for the provided user ID: " + uid); } return new UserRecord(response.getUsers().get(0), jsonFactory); @@ -159,7 +126,8 @@ UserRecord getUserByEmail(String email) throws FirebaseAuthException { GetAccountInfoResponse response = post( "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException(USER_NOT_FOUND_ERROR, + throw new FirebaseAuthException( + AuthHttpClient.USER_NOT_FOUND_ERROR, "No user record found for the provided email: " + email); } return new UserRecord(response.getUsers().get(0), jsonFactory); @@ -171,7 +139,8 @@ UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException GetAccountInfoResponse response = post( "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException(USER_NOT_FOUND_ERROR, + throw new FirebaseAuthException( + AuthHttpClient.USER_NOT_FOUND_ERROR, "No user record found for the provided phone number: " + phoneNumber); } return new UserRecord(response.getUsers().get(0), jsonFactory); @@ -180,7 +149,7 @@ UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException Set getAccountInfo(@NonNull Collection identifiers) throws FirebaseAuthException { if (identifiers.isEmpty()) { - return new HashSet(); + return new HashSet<>(); } GetAccountInfoRequest payload = new GetAccountInfoRequest(); @@ -192,7 +161,8 @@ Set getAccountInfo(@NonNull Collection identifiers) "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to parse server response"); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to parse server response"); } Set results = new HashSet<>(); @@ -204,7 +174,7 @@ Set getAccountInfo(@NonNull Collection identifiers) return results; } - String createUser(CreateRequest request) throws FirebaseAuthException { + String createUser(UserRecord.CreateRequest request) throws FirebaseAuthException { GenericJson response = post( "/accounts", request.getProperties(), GenericJson.class); if (response != null) { @@ -213,14 +183,16 @@ String createUser(CreateRequest request) throws FirebaseAuthException { return uid; } } - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create new user"); + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to create new user"); } - void updateUser(UpdateRequest request, JsonFactory jsonFactory) throws FirebaseAuthException { + void updateUser(UserRecord.UpdateRequest request, JsonFactory jsonFactory) + throws FirebaseAuthException { GenericJson response = post( "/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()); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to update user: " + request.getUid()); } } @@ -229,7 +201,8 @@ void deleteUser(String uid) throws FirebaseAuthException { GenericJson response = post( "/accounts:delete", payload, GenericJson.class); if (response == null || !response.containsKey("kind")) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete user: " + uid); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to delete user: " + uid); } } @@ -238,13 +211,13 @@ void deleteUser(String uid) throws FirebaseAuthException { * @pre uids.size() <= MAX_DELETE_ACCOUNTS_BATCH_SIZE */ DeleteUsersResult deleteUsers(@NonNull List uids) throws FirebaseAuthException { - final Map payload = ImmutableMap.of( + final Map payload = ImmutableMap.of( "localIds", uids, "force", true); BatchDeleteResponse response = post( "/accounts:batchDelete", payload, BatchDeleteResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete users"); + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to delete users"); } return new DeleteUsersResult(uids.size(), response); @@ -258,12 +231,12 @@ DownloadAccountResponse listUsers(int maxResults, String pageToken) throws Fireb builder.put("nextPageToken", pageToken); } - GenericUrl url = new GenericUrl(baseUrl + "/accounts:batchGet"); + GenericUrl url = new GenericUrl(userMgtBaseUrl + "/accounts:batchGet"); url.putAll(builder.build()); - DownloadAccountResponse response = sendRequest( + DownloadAccountResponse response = httpClient.sendRequest( "GET", url, null, DownloadAccountResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve users."); + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve users."); } return response; } @@ -273,7 +246,7 @@ UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthExcep UploadAccountResponse response = post( "/accounts:batchCreate", request, UploadAccountResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to import users."); + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to import users."); } return new UserImportResult(request.getUsersCount(), response); } @@ -289,7 +262,8 @@ String createSessionCookie(String idToken, return cookie; } } - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create session cookie"); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to create session cookie"); } String getEmailActionLink(EmailLinkType type, String email, @@ -308,64 +282,119 @@ String getEmailActionLink(EmailLinkType type, String email, return link; } } - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create email action link"); + throw new FirebaseAuthException( + AuthHttpClient.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"); - GenericUrl url = new GenericUrl(baseUrl + path); - return sendRequest("POST", url, content, clazz); + OidcProviderConfig createOidcProviderConfig( + OidcProviderConfig.CreateRequest request) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); + url.set("oauthIdpConfigId", request.getProviderId()); + return httpClient.sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class); } - 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 { - 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); - response = request.execute(); - return response.parseAs(clazz); - } catch (HttpResponseException e) { - // Server responded with an HTTP error - handleHttpError(e); - return null; - } catch (IOException e) { - // All other IO errors (Connection refused, reset, parse error etc.) + SamlProviderConfig createSamlProviderConfig( + SamlProviderConfig.CreateRequest request) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs"); + url.set("inboundSamlConfigId", request.getProviderId()); + return httpClient.sendRequest("POST", url, request.getProperties(), SamlProviderConfig.class); + } + + OidcProviderConfig updateOidcProviderConfig(OidcProviderConfig.UpdateRequest request) + throws FirebaseAuthException { + Map properties = request.getProperties(); + GenericUrl url = + new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(request.getProviderId())); + url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest("PATCH", url, properties, OidcProviderConfig.class); + } + + SamlProviderConfig updateSamlProviderConfig(SamlProviderConfig.UpdateRequest request) + throws FirebaseAuthException { + Map properties = request.getProperties(); + GenericUrl url = + new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(request.getProviderId())); + url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest("PATCH", url, properties, SamlProviderConfig.class); + } + + OidcProviderConfig getOidcProviderConfig(String providerId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); + return httpClient.sendRequest("GET", url, null, OidcProviderConfig.class); + } + + SamlProviderConfig getSamlProviderConfig(String providerId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId)); + return httpClient.sendRequest("GET", url, null, SamlProviderConfig.class); + } + + ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String pageToken) + throws FirebaseAuthException { + ImmutableMap.Builder builder = + ImmutableMap.builder().put("pageSize", maxResults); + if (pageToken != null) { + checkArgument(!pageToken.equals( + ListProviderConfigsPage.END_OF_LIST), "Invalid end of list page token."); + builder.put("nextPageToken", pageToken); + } + + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); + url.putAll(builder.build()); + ListOidcProviderConfigsResponse response = + httpClient.sendRequest("GET", url, null, ListOidcProviderConfigsResponse.class); + if (response == null) { throw new FirebaseAuthException( - INTERNAL_ERROR, "Error while calling user management backend service", e); - } finally { - if (response != null) { - try { - response.disconnect(); - } catch (IOException ignored) { - // Ignored - } - } + AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve provider configs."); } + return response; } - private void handleHttpError(HttpResponseException e) throws FirebaseAuthException { - try { - HttpErrorResponse response = jsonFactory.fromString(e.getContent(), HttpErrorResponse.class); - String code = ERROR_CODES.get(response.getErrorCode()); - if (code != null) { - throw new FirebaseAuthException(code, "User management service responded with an error", e); - } - } catch (IOException ignored) { - // Ignored + ListSamlProviderConfigsResponse listSamlProviderConfigs(int maxResults, String pageToken) + throws FirebaseAuthException { + ImmutableMap.Builder builder = + ImmutableMap.builder().put("pageSize", maxResults); + if (pageToken != null) { + checkArgument(!pageToken.equals( + ListProviderConfigsPage.END_OF_LIST), "Invalid end of list page token."); + builder.put("nextPageToken", pageToken); + } + + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs"); + url.putAll(builder.build()); + ListSamlProviderConfigsResponse response = + httpClient.sendRequest("GET", url, null, ListSamlProviderConfigsResponse.class); + if (response == null) { + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve provider configs."); } - String msg = String.format( - "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); - throw new FirebaseAuthException(INTERNAL_ERROR, msg, e); + return response; + } + + void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); + httpClient.sendRequest("DELETE", url, null, GenericJson.class); + } + + void deleteSamlProviderConfig(String providerId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId)); + httpClient.sendRequest("DELETE", url, null, GenericJson.class); + } + + private static String getOidcUrlSuffix(String providerId) { + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + return "/oauthIdpConfigs/" + providerId; + } + + private static String getSamlUrlSuffix(String providerId) { + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + return "/inboundSamlConfigs/" + providerId; + } + + 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"); + GenericUrl url = new GenericUrl(userMgtBaseUrl + path); + return httpClient.sendRequest("POST", url, content, clazz); } static class UserImportRequest extends GenericJson { @@ -407,4 +436,34 @@ enum EmailLinkType { EMAIL_SIGNIN, PASSWORD_RESET, } + + static Builder builder() { + return new Builder(); + } + + static class Builder { + + private FirebaseApp app; + private String tenantId; + private HttpRequestFactory requestFactory; + + Builder setFirebaseApp(FirebaseApp app) { + this.app = app; + return this; + } + + Builder setTenantId(String tenantId) { + this.tenantId = tenantId; + return this; + } + + Builder setHttpRequestFactory(HttpRequestFactory requestFactory) { + this.requestFactory = requestFactory; + return this; + } + + FirebaseUserManager build() { + return new FirebaseUserManager(this); + } + } } diff --git a/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java new file mode 100644 index 000000000..361f932bd --- /dev/null +++ b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java @@ -0,0 +1,268 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.gax.paging.Page; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse; +import com.google.firebase.auth.internal.ListProviderConfigsResponse; +import com.google.firebase.auth.internal.ListSamlProviderConfigsResponse; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Represents a page of {@link ProviderConfig} instances. + * + *

      Provides methods for iterating over the provider configs in the current page, and calling up + * subsequent pages of provider configs. + * + *

      Instances of this class are thread-safe and immutable. + */ +public class ListProviderConfigsPage implements Page { + + static final String END_OF_LIST = ""; + + private final ListProviderConfigsResponse currentBatch; + private final ProviderConfigSource source; + private final int maxResults; + + private ListProviderConfigsPage( + @NonNull ListProviderConfigsResponse currentBatch, + @NonNull ProviderConfigSource source, + int maxResults) { + this.currentBatch = checkNotNull(currentBatch); + this.source = checkNotNull(source); + this.maxResults = maxResults; + } + + /** + * Checks if there is another page of provider configs available to retrieve. + * + * @return true if another page is available, or false otherwise. + */ + @Override + public boolean hasNextPage() { + return !END_OF_LIST.equals(currentBatch.getPageToken()); + } + + /** + * Returns the string token that identifies the next page. + * + *

      Never returns null. Returns empty string if there are no more pages available to be + * retrieved. + * + * @return A non-null string token (possibly empty, representing no more pages) + */ + @NonNull + @Override + public String getNextPageToken() { + return currentBatch.getPageToken(); + } + + /** + * Returns the next page of provider configs. + * + * @return A new {@link ListProviderConfigsPage} instance, or null if there are no more pages. + */ + @Nullable + @Override + public ListProviderConfigsPage getNextPage() { + if (hasNextPage()) { + Factory factory = new Factory(source, maxResults, currentBatch.getPageToken()); + try { + return factory.create(); + } catch (FirebaseAuthException e) { + throw new RuntimeException(e); + } + } + return null; + } + + /** + * Returns an {@link Iterable} that facilitates transparently iterating over all the provider + * configs in the current Firebase project, starting from this page. + * + *

      The {@link Iterator} instances produced by the returned {@link Iterable} never buffers more + * than one page of provider configs at a time. It is safe to abandon the iterators (i.e. break + * the loops) at any time. + * + * @return a new {@link Iterable} instance. + */ + @NonNull + @Override + public Iterable iterateAll() { + return new ProviderConfigIterable(this); + } + + /** + * Returns an {@link Iterable} over the provider configs in this page. + * + * @return a {@link Iterable} instance. + */ + @NonNull + @Override + public Iterable getValues() { + return currentBatch.getProviderConfigs(); + } + + private static class ProviderConfigIterable implements Iterable { + + private final ListProviderConfigsPage startingPage; + + ProviderConfigIterable(@NonNull ListProviderConfigsPage startingPage) { + this.startingPage = checkNotNull(startingPage, "starting page must not be null"); + } + + @Override + @NonNull + public Iterator iterator() { + return new ProviderConfigIterator(startingPage); + } + + /** + * An {@link Iterator} that cycles through provider configs, one at a time. + * + *

      It buffers the last retrieved batch of provider configs in memory. The {@code maxResults} + * parameter is an upper bound on the batch size. + */ + private static class ProviderConfigIterator implements Iterator { + + private ListProviderConfigsPage currentPage; + private List batch; + private int index = 0; + + private ProviderConfigIterator(ListProviderConfigsPage startingPage) { + setCurrentPage(startingPage); + } + + @Override + public boolean hasNext() { + if (index == batch.size()) { + if (currentPage.hasNextPage()) { + setCurrentPage(currentPage.getNextPage()); + } else { + return false; + } + } + + return index < batch.size(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return batch.get(index++); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove operation not supported"); + } + + private void setCurrentPage(ListProviderConfigsPage page) { + this.currentPage = checkNotNull(page); + this.batch = ImmutableList.copyOf(page.getValues()); + this.index = 0; + } + } + } + + /** + * Represents a source of provider config data that can be queried to load a batch of provider + * configs. + */ + interface ProviderConfigSource { + @NonNull + ListProviderConfigsResponse fetch(int maxResults, String pageToken) + throws FirebaseAuthException; + } + + static class DefaultOidcProviderConfigSource implements ProviderConfigSource { + + private final FirebaseUserManager userManager; + + DefaultOidcProviderConfigSource(FirebaseUserManager userManager) { + this.userManager = checkNotNull(userManager, "User manager must not be null."); + } + + @Override + public ListOidcProviderConfigsResponse fetch(int maxResults, String pageToken) + throws FirebaseAuthException { + return userManager.listOidcProviderConfigs(maxResults, pageToken); + } + } + + static class DefaultSamlProviderConfigSource implements ProviderConfigSource { + + private final FirebaseUserManager userManager; + + DefaultSamlProviderConfigSource(FirebaseUserManager userManager) { + this.userManager = checkNotNull(userManager, "User manager must not be null."); + } + + @Override + public ListSamlProviderConfigsResponse fetch(int maxResults, String pageToken) + throws FirebaseAuthException { + return userManager.listSamlProviderConfigs(maxResults, pageToken); + } + } + + /** + * A simple factory class for {@link ProviderConfigsPage} instances. + * + *

      Performs argument validation before attempting to load any provider config data (which is + * expensive, and hence may be performed asynchronously on a separate thread). + */ + static class Factory { + + private final ProviderConfigSource source; + private final int maxResults; + private final String pageToken; + + Factory(@NonNull ProviderConfigSource source) { + this(source, FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS, null); + } + + Factory( + @NonNull ProviderConfigSource source, + int maxResults, + @Nullable String pageToken) { + checkArgument( + maxResults > 0 && maxResults <= FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS, + "maxResults must be a positive integer that does not exceed %s", + FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS); + checkArgument(!END_OF_LIST.equals(pageToken), "invalid end of list page token"); + this.source = checkNotNull(source, "source must not be null"); + this.maxResults = maxResults; + this.pageToken = pageToken; + } + + ListProviderConfigsPage create() throws FirebaseAuthException { + ListProviderConfigsResponse batch = source.fetch(maxResults, pageToken); + return new ListProviderConfigsPage(batch, source, maxResults); + } + } +} + diff --git a/src/main/java/com/google/firebase/auth/ListUsersPage.java b/src/main/java/com/google/firebase/auth/ListUsersPage.java index f406366ba..ba727af5a 100644 --- a/src/main/java/com/google/firebase/auth/ListUsersPage.java +++ b/src/main/java/com/google/firebase/auth/ListUsersPage.java @@ -80,7 +80,7 @@ public String getNextPageToken() { @Override public ListUsersPage getNextPage() { if (hasNextPage()) { - PageFactory factory = new PageFactory(source, maxResults, currentBatch.getNextPageToken()); + Factory factory = new Factory(source, maxResults, currentBatch.getNextPageToken()); try { return factory.create(); } catch (FirebaseAuthException e) { @@ -237,17 +237,17 @@ String getNextPageToken() { * before attempting to load any user data (which is expensive, and hence may be performed * asynchronously on a separate thread). */ - static class PageFactory { + static class Factory { private final UserSource source; private final int maxResults; private final String pageToken; - PageFactory(@NonNull UserSource source) { + Factory(@NonNull UserSource source) { this(source, FirebaseUserManager.MAX_LIST_USERS_RESULTS, null); } - PageFactory(@NonNull UserSource source, int maxResults, @Nullable String pageToken) { + Factory(@NonNull UserSource source, int maxResults, @Nullable String pageToken) { checkArgument(maxResults > 0 && maxResults <= FirebaseUserManager.MAX_LIST_USERS_RESULTS, "maxResults must be a positive integer that does not exceed %s", FirebaseUserManager.MAX_LIST_USERS_RESULTS); diff --git a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java new file mode 100644 index 000000000..879b7e79f --- /dev/null +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -0,0 +1,177 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.util.Key; +import com.google.common.base.Strings; + +/** + * Contains metadata associated with an OIDC Auth provider. + * + *

      Instances of this class are immutable and thread safe. + */ +public final class OidcProviderConfig extends ProviderConfig { + + @Key("clientId") + private String clientId; + + @Key("issuer") + private String issuer; + + public String getClientId() { + return clientId; + } + + public String getIssuer() { + return issuer; + } + + /** + * Returns a new {@link UpdateRequest}, which can be used to update the attributes of this + * provider config. + * + * @return A non-null {@link UpdateRequest} instance. + */ + public UpdateRequest updateRequest() { + return new UpdateRequest(getProviderId()); + } + + static void checkOidcProviderId(String providerId) { + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + checkArgument(providerId.startsWith("oidc."), + "Invalid OIDC provider ID (must be prefixed with 'oidc.'): " + providerId); + } + + /** + * A specification class for creating a new OIDC Auth provider. + * + *

      Set the initial attributes of the new provider by calling various setter methods available + * in this class. + */ + public static final class CreateRequest extends AbstractCreateRequest { + + /** + * Creates a new {@link CreateRequest}, which can be used to create a new OIDC Auth provider. + * + *

      The returned object should be passed to + * {@link AbstractFirebaseAuth#createOidcProviderConfig(CreateRequest)} to save the config. + */ + public CreateRequest() { } + + /** + * Sets the ID for the new provider. + * + * @param providerId A non-null, non-empty provider ID string. + * @throws IllegalArgumentException If the provider ID is null or empty, or is not prefixed with + * 'oidc.'. + */ + @Override + public CreateRequest setProviderId(String providerId) { + checkOidcProviderId(providerId); + return super.setProviderId(providerId); + } + + /** + * Sets the client ID for the new provider. + * + * @param clientId A non-null, non-empty client ID string. + * @throws IllegalArgumentException If the client ID is null or empty. + */ + public CreateRequest setClientId(String clientId) { + checkArgument(!Strings.isNullOrEmpty(clientId), "Client ID must not be null or empty."); + properties.put("clientId", clientId); + return this; + } + + /** + * Sets the issuer for the new provider. + * + * @param issuer A non-null, non-empty issuer URL string. + * @throws IllegalArgumentException If the issuer URL is null or empty, or if the format is + * invalid. + */ + public CreateRequest setIssuer(String issuer) { + checkArgument(!Strings.isNullOrEmpty(issuer), "Issuer must not be null or empty."); + assertValidUrl(issuer); + properties.put("issuer", issuer); + return this; + } + + CreateRequest getThis() { + return this; + } + } + + /** + * A specification class for updating an existing OIDC Auth provider. + * + *

      An instance of this class can be obtained via a {@link OidcProviderConfig} object, or from + * a provider ID string. Specify the changes to be made to the provider config by calling the + * various setter methods available in this class. + */ + public static final class UpdateRequest extends AbstractUpdateRequest { + + /** + * Creates a new {@link UpdateRequest}, which can be used to updates an existing OIDC Auth + * provider. + * + *

      The returned object should be passed to + * {@link AbstractFirebaseAuth#updateOidcProviderConfig(CreateRequest)} to save the updated + * config. + * + * @param providerId A non-null, non-empty provider ID string. + * @throws IllegalArgumentException If the provider ID is null or empty, or is not prefixed with + * "oidc.". + */ + public UpdateRequest(String providerId) { + super(providerId); + checkOidcProviderId(providerId); + } + + /** + * Sets the client ID for the exsting provider. + * + * @param clientId A non-null, non-empty client ID string. + * @throws IllegalArgumentException If the client ID is null or empty. + */ + public UpdateRequest setClientId(String clientId) { + checkArgument(!Strings.isNullOrEmpty(clientId), "Client ID must not be null or empty."); + properties.put("clientId", clientId); + return this; + } + + /** + * Sets the issuer for the existing provider. + * + * @param issuer A non-null, non-empty issuer URL string. + * @throws IllegalArgumentException If the issuer URL is null or empty, or if the format is + * invalid. + */ + public UpdateRequest setIssuer(String issuer) { + checkArgument(!Strings.isNullOrEmpty(issuer), "Issuer must not be null or empty."); + assertValidUrl(issuer); + properties.put("issuer", issuer); + return this; + } + + UpdateRequest getThis() { + return this; + } + } +} diff --git a/src/main/java/com/google/firebase/auth/ProviderConfig.java b/src/main/java/com/google/firebase/auth/ProviderConfig.java new file mode 100644 index 000000000..921a07b5b --- /dev/null +++ b/src/main/java/com/google/firebase/auth/ProviderConfig.java @@ -0,0 +1,157 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +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.ImmutableMap; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * The base class for Auth providers. + */ +public abstract class ProviderConfig { + + @Key("name") + private String resourceName; + + @Key("displayName") + private String displayName; + + @Key("enabled") + private boolean enabled; + + public String getProviderId() { + return resourceName.substring(resourceName.lastIndexOf("/") + 1); + } + + public String getDisplayName() { + return displayName; + } + + public boolean isEnabled() { + return enabled; + } + + static void assertValidUrl(String url) throws IllegalArgumentException { + try { + new URL(url); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(url + " is a malformed URL.", e); + } + } + + /** + * A base specification class for creating a new provider. + * + *

      Set the initial attributes of the new provider by calling various setter methods available + * in this class. + */ + public abstract static class AbstractCreateRequest> { + + final Map properties = new HashMap<>(); + String providerId; + + T setProviderId(String providerId) { + this.providerId = providerId; + return getThis(); + } + + String getProviderId() { + return providerId; + } + + /** + * Sets the display name for the new provider. + * + * @param displayName A non-null, non-empty display name string. + * @throws IllegalArgumentException If the display name is null or empty. + */ + public T setDisplayName(String displayName) { + checkArgument(!Strings.isNullOrEmpty(displayName), "Display name must not be null or empty."); + properties.put("displayName", displayName); + return getThis(); + } + + /** + * Sets whether to allow the user to sign in with the provider. + * + * @param enabled A boolean indicating whether the user can sign in with the provider. + */ + public T setEnabled(boolean enabled) { + properties.put("enabled", enabled); + return getThis(); + } + + Map getProperties() { + return ImmutableMap.copyOf(properties); + } + + abstract T getThis(); + } + + /** + * A base class for updating the attributes of an existing provider. + */ + public abstract static class AbstractUpdateRequest> { + + final String providerId; + final Map properties = new HashMap<>(); + + AbstractUpdateRequest(String providerId) { + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + this.providerId = providerId; + } + + String getProviderId() { + return providerId; + } + + /** + * Sets the display name for the existing provider. + * + * @param displayName A non-null, non-empty display name string. + * @throws IllegalArgumentException If the display name is null or empty. + */ + public T setDisplayName(String displayName) { + checkArgument(!Strings.isNullOrEmpty(displayName), "Display name must not be null or empty."); + properties.put("displayName", displayName); + return getThis(); + } + + /** + * Sets whether to allow the user to sign in with the provider. + * + * @param enabled A boolean indicating whether the user can sign in with the provider. + */ + public T setEnabled(boolean enabled) { + properties.put("enabled", enabled); + return getThis(); + } + + Map getProperties() { + return ImmutableMap.copyOf(properties); + } + + abstract T getThis(); + } +} diff --git a/src/main/java/com/google/firebase/auth/SamlProviderConfig.java b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java new file mode 100644 index 000000000..76f8594a8 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java @@ -0,0 +1,343 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +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.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Contains metadata associated with a SAML Auth provider. + * + *

      Instances of this class are immutable and thread safe. + */ +public final class SamlProviderConfig extends ProviderConfig { + + @Key("idpConfig") + private GenericJson idpConfig; + + @Key("spConfig") + private GenericJson spConfig; + + public String getIdpEntityId() { + return (String) idpConfig.get("idpEntityId"); + } + + public String getSsoUrl() { + return (String) idpConfig.get("ssoUrl"); + } + + public List getX509Certificates() { + List> idpCertificates = + (List>) idpConfig.get("idpCertificates"); + checkNotNull(idpCertificates); + ImmutableList.Builder certificates = ImmutableList.builder(); + for (Map idpCertificate : idpCertificates) { + certificates.add(idpCertificate.get("x509Certificate")); + } + return certificates.build(); + } + + public String getRpEntityId() { + return (String) spConfig.get("spEntityId"); + } + + public String getCallbackUrl() { + return (String) spConfig.get("callbackUri"); + } + + /** + * Returns a new {@link UpdateRequest}, which can be used to update the attributes of this + * provider config. + * + * @return a non-null {@link UpdateRequest} instance. + */ + public UpdateRequest updateRequest() { + return new UpdateRequest(getProviderId()); + } + + static void checkSamlProviderId(String providerId) { + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + checkArgument(providerId.startsWith("saml."), + "Invalid SAML provider ID (must be prefixed with 'saml.'): " + providerId); + } + + private static List ensureNestedList(Map outerMap, String id) { + List list = (List) outerMap.get(id); + if (list == null) { + list = new ArrayList(); + outerMap.put(id, list); + } + return list; + } + + private static Map ensureNestedMap(Map outerMap, String id) { + Map map = (Map) outerMap.get(id); + if (map == null) { + map = new HashMap(); + outerMap.put(id, map); + } + return map; + } + + /** + * A specification class for creating a new SAML Auth provider. + * + *

      Set the initial attributes of the new provider by calling various setter methods available + * in this class. + */ + public static final class CreateRequest extends AbstractCreateRequest { + + /** + * Creates a new {@link CreateRequest}, which can be used to create a new SAML Auth provider. + * + *

      The returned object should be passed to + * {@link AbstractFirebaseAuth#createSamlProviderConfig(CreateRequest)} to register the provider + * information persistently. + */ + public CreateRequest() { } + + /** + * Sets the ID for the new provider. + * + * @param providerId A non-null, non-empty provider ID string. + * @throws IllegalArgumentException If the provider ID is null or empty, or is not prefixed with + * 'saml.'. + */ + @Override + public CreateRequest setProviderId(String providerId) { + checkSamlProviderId(providerId); + return super.setProviderId(providerId); + } + + /** + * Sets the IDP entity ID for the new provider. + * + * @param idpEntityId A non-null, non-empty IDP entity ID string. + * @throws IllegalArgumentException If the IDP entity ID is null or empty. + */ + public CreateRequest setIdpEntityId(String idpEntityId) { + checkArgument(!Strings.isNullOrEmpty(idpEntityId), + "IDP entity ID must not be null or empty."); + ensureNestedMap(properties, "idpConfig").put("idpEntityId", idpEntityId); + return this; + } + + /** + * Sets the SSO URL for the new provider. + * + * @param ssoUrl A non-null, non-empty SSO URL string. + * @throws IllegalArgumentException If the SSO URL is null or empty, or if the format is + * invalid. + */ + public CreateRequest setSsoUrl(String ssoUrl) { + checkArgument(!Strings.isNullOrEmpty(ssoUrl), "SSO URL must not be null or empty."); + assertValidUrl(ssoUrl); + ensureNestedMap(properties, "idpConfig").put("ssoUrl", ssoUrl); + return this; + } + + /** + * Adds a x509 certificate to the new provider. + * + * @param x509Certificate A non-null, non-empty x509 certificate string. + * @throws IllegalArgumentException If the x509 certificate is null or empty. + */ + public CreateRequest addX509Certificate(String x509Certificate) { + checkArgument(!Strings.isNullOrEmpty(x509Certificate), + "The x509 certificate must not be null or empty."); + Map idpConfigProperties = ensureNestedMap(properties, "idpConfig"); + List x509Certificates = ensureNestedList(idpConfigProperties, "idpCertificates"); + x509Certificates.add(ImmutableMap.of("x509Certificate", x509Certificate)); + return this; + } + + /** + * Adds a collection of x509 certificates to the new provider. + * + * @param x509Certificates A non-null, non-empty collection of x509 certificate strings. + * @throws IllegalArgumentException If the collection is null or empty, or if any x509 + * certificates are null or empty. + */ + public CreateRequest addAllX509Certificates(Collection x509Certificates) { + checkArgument(x509Certificates != null, + "The collection of x509 certificates must not be null."); + checkArgument(!x509Certificates.isEmpty(), + "The collection of x509 certificates must not be empty."); + for (String certificate : x509Certificates) { + addX509Certificate(certificate); + } + return this; + } + + /** + * Sets the RP entity ID for the new provider. + * + * @param rpEntityId A non-null, non-empty RP entity ID string. + * @throws IllegalArgumentException If the RP entity ID is null or empty. + */ + public CreateRequest setRpEntityId(String rpEntityId) { + checkArgument(!Strings.isNullOrEmpty(rpEntityId), "RP entity ID must not be null or empty."); + ensureNestedMap(properties, "spConfig").put("spEntityId", rpEntityId); + return this; + } + + /** + * Sets the callback URL for the new provider. + * + * @param callbackUrl A non-null, non-empty callback URL string. + * @throws IllegalArgumentException If the callback URL is null or empty, or if the format is + * invalid. + */ + public CreateRequest setCallbackUrl(String callbackUrl) { + checkArgument(!Strings.isNullOrEmpty(callbackUrl), "Callback URL must not be null or empty."); + assertValidUrl(callbackUrl); + ensureNestedMap(properties, "spConfig").put("callbackUri", callbackUrl); + return this; + } + + CreateRequest getThis() { + return this; + } + } + + /** + * A specification class for updating an existing SAML Auth provider. + * + *

      An instance of this class can be obtained via a {@link SamlProviderConfig} object, or from + * a provider ID string. Specify the changes to be made to the provider config by calling the + * various setter methods available in this class. + */ + public static final class UpdateRequest extends AbstractUpdateRequest { + /** + * Creates a new {@link UpdateRequest}, which can be used to updates an existing SAML Auth + * provider. + * + *

      The returned object should be passed to + * {@link AbstractFirebaseAuth#updateSamlProviderConfig(UpdateRequest)} to update the provider + * information persistently. + * + * @param providerId a non-null, non-empty provider ID string. + * @throws IllegalArgumentException If the provider ID is null or empty, or is not prefixed with + * 'saml.'. + */ + public UpdateRequest(String providerId) { + super(providerId); + checkSamlProviderId(providerId); + } + + /** + * Sets the IDP entity ID for the existing provider. + * + * @param idpEntityId A non-null, non-empty IDP entity ID string. + * @throws IllegalArgumentException If the IDP entity ID is null or empty. + */ + public UpdateRequest setIdpEntityId(String idpEntityId) { + checkArgument(!Strings.isNullOrEmpty(idpEntityId), + "IDP entity ID must not be null or empty."); + ensureNestedMap(properties, "idpConfig").put("idpEntityId", idpEntityId); + return this; + } + + /** + * Sets the SSO URL for the existing provider. + * + * @param ssoUrl A non-null, non-empty SSO URL string. + * @throws IllegalArgumentException If the SSO URL is null or empty, or if the format is + * invalid. + */ + public UpdateRequest setSsoUrl(String ssoUrl) { + checkArgument(!Strings.isNullOrEmpty(ssoUrl), "SSO URL must not be null or empty."); + assertValidUrl(ssoUrl); + ensureNestedMap(properties, "idpConfig").put("ssoUrl", ssoUrl); + return this; + } + + /** + * Adds a x509 certificate to the existing provider. + * + * @param x509Certificate A non-null, non-empty x509 certificate string. + * @throws IllegalArgumentException If the x509 certificate is null or empty. + */ + public UpdateRequest addX509Certificate(String x509Certificate) { + checkArgument(!Strings.isNullOrEmpty(x509Certificate), + "The x509 certificate must not be null or empty."); + Map idpConfigProperties = ensureNestedMap(properties, "idpConfig"); + List x509Certificates = ensureNestedList(idpConfigProperties, "idpCertificates"); + x509Certificates.add(ImmutableMap.of("x509Certificate", x509Certificate)); + return this; + } + + /** + * Adds a collection of x509 certificates to the existing provider. + * + * @param x509Certificates A non-null, non-empty collection of x509 certificate strings. + * @throws IllegalArgumentException If the collection is null or empty, or if any x509 + * certificates are null or empty. + */ + public UpdateRequest addAllX509Certificates(Collection x509Certificates) { + checkArgument(x509Certificates != null, + "The collection of x509 certificates must not be null."); + checkArgument(!x509Certificates.isEmpty(), + "The collection of x509 certificates must not be empty."); + for (String certificate : x509Certificates) { + addX509Certificate(certificate); + } + return this; + } + + /** + * Sets the RP entity ID for the existing provider. + * + * @param rpEntityId A non-null, non-empty RP entity ID string. + * @throws IllegalArgumentException If the RP entity ID is null or empty. + */ + public UpdateRequest setRpEntityId(String rpEntityId) { + checkArgument(!Strings.isNullOrEmpty(rpEntityId), "RP entity ID must not be null or empty."); + ensureNestedMap(properties, "spConfig").put("spEntityId", rpEntityId); + return this; + } + + /** + * Sets the callback URL for the exising provider. + * + * @param callbackUrl A non-null, non-empty callback URL string. + * @throws IllegalArgumentException If the callback URL is null or empty, or if the format is + * invalid. + */ + public UpdateRequest setCallbackUrl(String callbackUrl) { + checkArgument(!Strings.isNullOrEmpty(callbackUrl), "Callback URL must not be null or empty."); + assertValidUrl(callbackUrl); + ensureNestedMap(properties, "spConfig").put("callbackUri", callbackUrl); + return this; + } + + UpdateRequest getThis() { + return this; + } + } +} diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index 64e7c278c..0af08f65b 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -50,6 +50,7 @@ public class UserRecord implements UserInfo { private static final int MAX_CLAIMS_PAYLOAD_SIZE = 1000; private final String uid; + private final String tenantId; private final String email; private final String phoneNumber; private final boolean emailVerified; @@ -66,6 +67,7 @@ public class UserRecord implements UserInfo { checkNotNull(jsonFactory, "jsonFactory must not be null"); checkArgument(!Strings.isNullOrEmpty(response.getUid()), "uid must not be null or empty"); this.uid = response.getUid(); + this.tenantId = response.getTenantId(); this.email = response.getEmail(); this.phoneNumber = response.getPhoneNumber(); this.emailVerified = response.isEmailVerified(); @@ -116,6 +118,16 @@ public String getUid() { return uid; } + /** + * Returns the tenant ID associated with this user, if one exists. + * + * @return a tenant ID string or null. + */ + @Nullable + public String getTenantId() { + return this.tenantId; + } + /** * Returns the provider ID of this user. * @@ -199,9 +211,9 @@ public UserInfo[] getProviderData() { } /** - * Returns a timestamp in milliseconds since epoch, truncated down to the closest second. + * Returns a timestamp in milliseconds since epoch, truncated down to the closest second. * Tokens minted before this timestamp are considered invalid. - * + * * @return Timestamp in milliseconds since the epoch. Tokens minted before this timestamp are * considered invalid. */ @@ -371,10 +383,10 @@ public CreateRequest setEmailVerified(boolean emailVerified) { /** * Sets the display name for the new user. * - * @param displayName a non-null, non-empty display name string. + * @param displayName a non-null display name string. */ public CreateRequest setDisplayName(String displayName) { - checkNotNull(displayName, "displayName cannot be null or empty"); + checkNotNull(displayName, "displayName cannot be null"); properties.put("displayName", displayName); return this; } diff --git a/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java new file mode 100644 index 000000000..ad77236d1 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java @@ -0,0 +1,160 @@ +/* + * Copyright 2020 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 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.HttpContent; +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.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedSet; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.internal.Nullable; +import com.google.firebase.internal.SdkUtils; +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +/** + * Provides a convenient API for making REST calls to the Firebase Auth backend servers. + */ +public final class AuthHttpClient { + + public static final String CONFIGURATION_NOT_FOUND_ERROR = "configuration-not-found"; + public static final String INTERNAL_ERROR = "internal-error"; + public static final String TENANT_NOT_FOUND_ERROR = "tenant-not-found"; + public static final String USER_NOT_FOUND_ERROR = "user-not-found"; + + private static final String CLIENT_VERSION_HEADER = "X-Client-Version"; + + private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion(); + + // Map of server-side error codes to SDK error codes. + // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors + private static final Map ERROR_CODES = ImmutableMap.builder() + .put("CLAIMS_TOO_LARGE", "claims-too-large") + .put("CONFIGURATION_NOT_FOUND", CONFIGURATION_NOT_FOUND_ERROR) + .put("INSUFFICIENT_PERMISSION", "insufficient-permission") + .put("DUPLICATE_EMAIL", "email-already-exists") + .put("DUPLICATE_LOCAL_ID", "uid-already-exists") + .put("EMAIL_EXISTS", "email-already-exists") + .put("INVALID_CLAIMS", "invalid-claims") + .put("INVALID_EMAIL", "invalid-email") + .put("INVALID_PAGE_SELECTION", "invalid-page-token") + .put("INVALID_PHONE_NUMBER", "invalid-phone-number") + .put("PHONE_NUMBER_EXISTS", "phone-number-already-exists") + .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") + .put("TENANT_NOT_FOUND", TENANT_NOT_FOUND_ERROR) + .build(); + + private final JsonFactory jsonFactory; + private final HttpRequestFactory requestFactory; + + private HttpResponseInterceptor interceptor; + + public AuthHttpClient(JsonFactory jsonFactory, HttpRequestFactory requestFactory) { + this.jsonFactory = jsonFactory; + this.requestFactory = requestFactory; + } + + public static Set generateMask(Map properties) { + ImmutableSortedSet.Builder maskBuilder = ImmutableSortedSet.naturalOrder(); + for (Map.Entry entry : properties.entrySet()) { + if (entry.getValue() instanceof Map) { + Set childMask = generateMask((Map) entry.getValue()); + for (String childProperty : childMask) { + maskBuilder.add(entry.getKey() + "." + childProperty); + } + } else { + maskBuilder.add(entry.getKey()); + } + } + return maskBuilder.build(); + } + + public void setInterceptor(HttpResponseInterceptor interceptor) { + this.interceptor = interceptor; + } + + public 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 { + HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null; + HttpRequest request = + requestFactory.buildRequest(method.equals("PATCH") ? "POST" : method, url, httpContent); + request.setParser(new JsonObjectParser(jsonFactory)); + request.getHeaders().set(CLIENT_VERSION_HEADER, CLIENT_VERSION); + if (method.equals("PATCH")) { + request.getHeaders().set("X-HTTP-Method-Override", "PATCH"); + } + request.setResponseInterceptor(interceptor); + response = request.execute(); + return response.parseAs(clazz); + } catch (HttpResponseException e) { + // Server responded with an HTTP error + handleHttpError(e); + return null; + } catch (IOException e) { + // All other IO errors (Connection refused, reset, parse error etc.) + throw new FirebaseAuthException( + INTERNAL_ERROR, "Error while calling the Firebase Auth backend service", e); + } finally { + if (response != null) { + try { + response.disconnect(); + } catch (IOException ignored) { + // Ignored + } + } + } + } + + private void handleHttpError(HttpResponseException e) throws FirebaseAuthException { + try { + HttpErrorResponse response = jsonFactory.fromString(e.getContent(), HttpErrorResponse.class); + String code = ERROR_CODES.get(response.getErrorCode()); + if (code != null) { + throw new FirebaseAuthException(code, "Firebase Auth service responded with an error", e); + } + } catch (IOException ignored) { + // Ignored + } + String msg = String.format( + "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); + throw new FirebaseAuthException(INTERNAL_ERROR, msg, e); + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/FirebaseCustomAuthToken.java b/src/main/java/com/google/firebase/auth/internal/FirebaseCustomAuthToken.java index e67576464..2fe0b1859 100644 --- a/src/main/java/com/google/firebase/auth/internal/FirebaseCustomAuthToken.java +++ b/src/main/java/com/google/firebase/auth/internal/FirebaseCustomAuthToken.java @@ -22,6 +22,7 @@ import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.api.client.util.Key; import com.google.firebase.auth.FirebaseToken; +import com.google.firebase.internal.Nullable; import java.io.IOException; @@ -77,6 +78,9 @@ public static class Payload extends IdToken.Payload { @Key("claims") private GenericJson developerClaims; + @Key("tenant_id") + private String tenantId; + public final String getUid() { return uid; } @@ -95,6 +99,15 @@ public Payload setDeveloperClaims(GenericJson developerClaims) { return this; } + public final String getTenantId() { + return tenantId; + } + + public Payload setTenantId(String tenantId) { + this.tenantId = tenantId; + return this; + } + @Override public Payload setIssuer(String issuer) { return (Payload) super.setIssuer(issuer); 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 95d313134..778911d46 100644 --- a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java +++ b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java @@ -25,8 +25,9 @@ import com.google.api.client.util.Base64; import com.google.api.client.util.Clock; import com.google.api.client.util.StringUtils; - import com.google.common.base.Strings; +import com.google.firebase.internal.Nullable; + import java.io.IOException; import java.util.Collection; import java.util.Map; @@ -41,11 +42,18 @@ public class FirebaseTokenFactory { private final JsonFactory jsonFactory; private final Clock clock; private final CryptoSigner signer; + private final String tenantId; public FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) { + this(jsonFactory, clock, signer, null); + } + + public FirebaseTokenFactory( + JsonFactory jsonFactory, Clock clock, CryptoSigner signer, @Nullable String tenantId) { this.jsonFactory = checkNotNull(jsonFactory); this.clock = checkNotNull(clock); this.signer = checkNotNull(signer); + this.tenantId = tenantId; } String createSignedCustomAuthTokenForUser(String uid) throws IOException { @@ -68,6 +76,9 @@ public String createSignedCustomAuthTokenForUser( .setAudience(FirebaseCustomAuthToken.FIREBASE_AUDIENCE) .setIssuedAtTimeSeconds(issuedAt) .setExpirationTimeSeconds(issuedAt + FirebaseCustomAuthToken.TOKEN_DURATION_SECONDS); + if (!Strings.isNullOrEmpty(tenantId)) { + payload.setTenantId(tenantId); + } if (developerClaims != null) { Collection reservedNames = payload.getClassInfo().getNames(); diff --git a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java index e84335891..7bde3eb39 100644 --- a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java +++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java @@ -46,6 +46,9 @@ public static class User { @Key("localId") private String uid; + @Key("tenantId") + private String tenantId; + @Key("email") private String email; @@ -86,6 +89,10 @@ public String getUid() { return uid; } + public String getTenantId() { + return tenantId; + } + public String getEmail() { return email; } @@ -129,7 +136,7 @@ public String getLastRefreshAt() { public long getValidSince() { return validSince; } - + public String getCustomClaims() { return customClaims; } diff --git a/src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java new file mode 100644 index 000000000..187f98cf5 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import com.google.api.client.util.Key; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.OidcProviderConfig; +import java.util.List; + +/** + * JSON data binding for ListOAuthIdpConfigsResponse messages sent by Google identity toolkit + * service. + */ +public final class ListOidcProviderConfigsResponse + implements ListProviderConfigsResponse { + + @Key("oauthIdpConfigs") + private List providerConfigs; + + @Key("nextPageToken") + private String pageToken; + + @VisibleForTesting + public ListOidcProviderConfigsResponse( + List providerConfigs, String pageToken) { + this.providerConfigs = providerConfigs; + this.pageToken = pageToken; + } + + public ListOidcProviderConfigsResponse() { } + + @Override + public List getProviderConfigs() { + return providerConfigs == null ? ImmutableList.of() : providerConfigs; + } + + @Override + public boolean hasProviderConfigs() { + return providerConfigs != null && !providerConfigs.isEmpty(); + } + + @Override + public String getPageToken() { + return Strings.nullToEmpty(pageToken); + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java new file mode 100644 index 000000000..2f25ae623 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import com.google.firebase.auth.ProviderConfig; +import java.util.List; + +/** + * Interface for config list response messages sent by Google identity toolkit service. + */ +public interface ListProviderConfigsResponse { + + public List getProviderConfigs(); + + public boolean hasProviderConfigs(); + + public String getPageToken(); +} diff --git a/src/main/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponse.java new file mode 100644 index 000000000..55b944d53 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponse.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import com.google.api.client.util.Key; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.SamlProviderConfig; +import java.util.List; + +/** + * JSON data binding for ListInboundSamlConfigsResponse messages sent by Google identity toolkit + * service. + */ +public final class ListSamlProviderConfigsResponse + implements ListProviderConfigsResponse { + + @Key("inboundSamlConfigs") + private List providerConfigs; + + @Key("nextPageToken") + private String pageToken; + + @VisibleForTesting + public ListSamlProviderConfigsResponse( + List providerConfigs, String pageToken) { + this.providerConfigs = providerConfigs; + this.pageToken = pageToken; + } + + public ListSamlProviderConfigsResponse() { } + + @Override + public List getProviderConfigs() { + return providerConfigs == null ? ImmutableList.of() : providerConfigs; + } + + @Override + public boolean hasProviderConfigs() { + return providerConfigs != null && !providerConfigs.isEmpty(); + } + + @Override + public String getPageToken() { + return Strings.nullToEmpty(pageToken); + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java new file mode 100644 index 000000000..f1086921f --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import com.google.api.client.util.Key; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.multitenancy.Tenant; +import java.util.List; + +/** + * JSON data binding for ListTenantsResponse messages sent by Google identity toolkit service. + */ +public final class ListTenantsResponse { + + @Key("tenants") + private List tenants; + + @Key("pageToken") + private String pageToken; + + @VisibleForTesting + public ListTenantsResponse(List tenants, String pageToken) { + this.tenants = tenants; + this.pageToken = pageToken; + } + + public ListTenantsResponse() { } + + public List getTenants() { + return tenants == null ? ImmutableList.of() : tenants; + } + + public boolean hasTenants() { + return tenants != null && !tenants.isEmpty(); + } + + public String getPageToken() { + return pageToken == null ? "" : pageToken; + } +} diff --git a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java new file mode 100644 index 000000000..1278e63d5 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java @@ -0,0 +1,111 @@ +/* + * Copyright 2020 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.multitenancy; + +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.HttpRequestFactory; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.auth.internal.ListTenantsResponse; +import com.google.firebase.internal.ApiClientUtils; +import java.util.Map; + +final class FirebaseTenantClient { + + static final int MAX_LIST_TENANTS_RESULTS = 100; + + private static final String ID_TOOLKIT_URL = + "https://identitytoolkit.googleapis.com/%s/projects/%s"; + + private final String tenantMgtBaseUrl; + private final AuthHttpClient httpClient; + + FirebaseTenantClient(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.tenantMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v2", projectId); + JsonFactory jsonFactory = app.getOptions().getJsonFactory(); + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + this.httpClient = new AuthHttpClient(jsonFactory, requestFactory); + } + + void setInterceptor(HttpResponseInterceptor interceptor) { + httpClient.setInterceptor(interceptor); + } + + Tenant getTenant(String tenantId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId)); + return httpClient.sendRequest("GET", url, null, Tenant.class); + } + + Tenant createTenant(Tenant.CreateRequest request) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants"); + return httpClient.sendRequest("POST", url, request.getProperties(), Tenant.class); + } + + Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException { + Map properties = request.getProperties(); + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(request.getTenantId())); + url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest("PATCH", url, properties, Tenant.class); + } + + void deleteTenant(String tenantId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId)); + httpClient.sendRequest("DELETE", url, null, GenericJson.class); + } + + ListTenantsResponse listTenants(int maxResults, String pageToken) + throws FirebaseAuthException { + ImmutableMap.Builder builder = + ImmutableMap.builder().put("pageSize", maxResults); + if (pageToken != null) { + checkArgument(!pageToken.equals( + ListTenantsPage.END_OF_LIST), "Invalid end of list page token."); + builder.put("pageToken", pageToken); + } + + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants"); + url.putAll(builder.build()); + ListTenantsResponse response = httpClient.sendRequest( + "GET", url, null, ListTenantsResponse.class); + if (response == null) { + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve tenants."); + } + return response; + } + + private static String getTenantUrlSuffix(String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + return "/tenants/" + tenantId; + } +} diff --git a/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java b/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java new file mode 100644 index 000000000..c1f393ddb --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java @@ -0,0 +1,245 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.gax.paging.Page; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.internal.ListTenantsResponse; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Represents a page of {@link Tenant} instances. + * + *

      Provides methods for iterating over the tenants in the current page, and calling up + * subsequent pages of tenants. + * + *

      Instances of this class are thread-safe and immutable. + */ +public class ListTenantsPage implements Page { + + static final String END_OF_LIST = ""; + + private final ListTenantsResponse currentBatch; + private final TenantSource source; + private final int maxResults; + + private ListTenantsPage( + @NonNull ListTenantsResponse currentBatch, @NonNull TenantSource source, int maxResults) { + this.currentBatch = checkNotNull(currentBatch); + this.source = checkNotNull(source); + this.maxResults = maxResults; + } + + /** + * Checks if there is another page of tenants available to retrieve. + * + * @return true if another page is available, or false otherwise. + */ + @Override + public boolean hasNextPage() { + return !END_OF_LIST.equals(currentBatch.getPageToken()); + } + + /** + * Returns the string token that identifies the next page. + * + *

      Never returns null. Returns empty string if there are no more pages available to be + * retrieved. + * + * @return A non-null string token (possibly empty, representing no more pages) + */ + @NonNull + @Override + public String getNextPageToken() { + return currentBatch.getPageToken(); + } + + /** + * Returns the next page of tenants. + * + * @return A new {@link ListTenantsPage} instance, or null if there are no more pages. + */ + @Nullable + @Override + public ListTenantsPage getNextPage() { + if (hasNextPage()) { + PageFactory factory = new PageFactory(source, maxResults, currentBatch.getPageToken()); + try { + return factory.create(); + } catch (FirebaseAuthException e) { + throw new RuntimeException(e); + } + } + return null; + } + + /** + * Returns an {@link Iterable} that facilitates transparently iterating over all the tenants in + * the current Firebase project, starting from this page. + * + *

      The {@link Iterator} instances produced by the returned {@link Iterable} never buffers more + * than one page of tenants at a time. It is safe to abandon the iterators (i.e. break the loops) + * at any time. + * + * @return a new {@link Iterable} instance. + */ + @NonNull + @Override + public Iterable iterateAll() { + return new TenantIterable(this); + } + + /** + * Returns an {@link Iterable} over the tenants in this page. + * + * @return a {@link Iterable} instance. + */ + @NonNull + @Override + public Iterable getValues() { + return currentBatch.getTenants(); + } + + private static class TenantIterable implements Iterable { + + private final ListTenantsPage startingPage; + + TenantIterable(@NonNull ListTenantsPage startingPage) { + this.startingPage = checkNotNull(startingPage, "starting page must not be null"); + } + + @Override + @NonNull + public Iterator iterator() { + return new TenantIterator(startingPage); + } + + /** + * An {@link Iterator} that cycles through tenants, one at a time. + * + *

      It buffers the last retrieved batch of tenants in memory. The {@code maxResults} parameter + * is an upper bound on the batch size. + */ + private static class TenantIterator implements Iterator { + + private ListTenantsPage currentPage; + private List batch; + private int index = 0; + + private TenantIterator(ListTenantsPage startingPage) { + setCurrentPage(startingPage); + } + + @Override + public boolean hasNext() { + if (index == batch.size()) { + if (currentPage.hasNextPage()) { + setCurrentPage(currentPage.getNextPage()); + } else { + return false; + } + } + + return index < batch.size(); + } + + @Override + public Tenant next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return batch.get(index++); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove operation not supported"); + } + + private void setCurrentPage(ListTenantsPage page) { + this.currentPage = checkNotNull(page); + this.batch = ImmutableList.copyOf(page.getValues()); + this.index = 0; + } + } + } + + /** + * Represents a source of tenant data that can be queried to load a batch of tenants. + */ + interface TenantSource { + @NonNull + ListTenantsResponse fetch(int maxResults, String pageToken) + throws FirebaseAuthException; + } + + static class DefaultTenantSource implements TenantSource { + + private final FirebaseTenantClient tenantClient; + + DefaultTenantSource(FirebaseTenantClient tenantClient) { + this.tenantClient = checkNotNull(tenantClient, "Tenant client must not be null."); + } + + @Override + public ListTenantsResponse fetch(int maxResults, String pageToken) + throws FirebaseAuthException { + return tenantClient.listTenants(maxResults, pageToken); + } + } + + /** + * A simple factory class for {@link ListTenantsPage} instances. + * + *

      Performs argument validation before attempting to load any tenant data (which is expensive, + * and hence may be performed asynchronously on a separate thread). + */ + static class PageFactory { + + private final TenantSource source; + private final int maxResults; + private final String pageToken; + + PageFactory(@NonNull TenantSource source) { + this(source, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS, null); + } + + PageFactory(@NonNull TenantSource source, int maxResults, @Nullable String pageToken) { + checkArgument(maxResults > 0 && maxResults <= FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS, + "maxResults must be a positive integer that does not exceed %s", + FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS); + checkArgument(!END_OF_LIST.equals(pageToken), "Invalid end of list page token."); + this.source = checkNotNull(source, "Source must not be null."); + this.maxResults = maxResults; + this.pageToken = pageToken; + } + + ListTenantsPage create() throws FirebaseAuthException { + ListTenantsResponse batch = source.fetch(maxResults, pageToken); + return new ListTenantsPage(batch, source, maxResults); + } + } +} + diff --git a/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java new file mode 100644 index 000000000..57d215e96 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java @@ -0,0 +1,195 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +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.ImmutableMap; +import java.util.HashMap; +import java.util.Map; + +/** + * Contains metadata associated with a Firebase tenant. + * + *

      Instances of this class are immutable and thread safe. + */ +public final class Tenant { + + @Key("name") + private String resourceName; + + @Key("displayName") + private String displayName; + + @Key("allowPasswordSignup") + private boolean passwordSignInAllowed; + + @Key("enableEmailLinkSignin") + private boolean emailLinkSignInEnabled; + + public String getTenantId() { + return resourceName.substring(resourceName.lastIndexOf("/") + 1); + } + + public String getDisplayName() { + return displayName; + } + + public boolean isPasswordSignInAllowed() { + return passwordSignInAllowed; + } + + public boolean isEmailLinkSignInEnabled() { + return emailLinkSignInEnabled; + } + + /** + * Returns a new {@link UpdateRequest}, which can be used to update the attributes of this tenant. + * + * @return a non-null {@link UpdateRequest} instance. + */ + public UpdateRequest updateRequest() { + return new UpdateRequest(getTenantId()); + } + + /** + * A specification class for creating a new tenant. + * + *

      Set the initial attributes of the new tenant by calling various setter methods available in + * this class. None of the attributes are required. + */ + public static final class CreateRequest { + + private final Map properties = new HashMap<>(); + + /** + * Creates a new {@link CreateRequest}, which can be used to create a new tenant. + * + *

      The returned object should be passed to {@link TenantManager#createTenant(CreateRequest)} + * to register the tenant information persistently. + */ + public CreateRequest() { } + + /** + * Sets the display name for the new tenant. + * + * @param displayName a non-null, non-empty display name string. + */ + public CreateRequest setDisplayName(String displayName) { + checkArgument(!Strings.isNullOrEmpty(displayName), "display name must not be null or empty"); + properties.put("displayName", displayName); + return this; + } + + /** + * Sets whether to allow email/password user authentication. + * + * @param passwordSignInAllowed a boolean indicating whether users can be authenticated using + * an email and password. + */ + public CreateRequest setPasswordSignInAllowed(boolean passwordSignInAllowed) { + properties.put("allowPasswordSignup", passwordSignInAllowed); + return this; + } + + /** + * Sets whether to enable email link user authentication. + * + * @param emailLinkSignInEnabled a boolean indicating whether users can be authenticated using + * an email link. + */ + public CreateRequest setEmailLinkSignInEnabled(boolean emailLinkSignInEnabled) { + properties.put("enableEmailLinkSignin", emailLinkSignInEnabled); + return this; + } + + Map getProperties() { + return ImmutableMap.copyOf(properties); + } + } + + /** + * A class for updating the attributes of an existing tenant. + * + *

      An instance of this class can be obtained via a {@link Tenant} object, or from a tenant ID + * string. Specify the changes to be made to the tenant by calling the various setter methods + * available in this class. + */ + public static final class UpdateRequest { + + private final String tenantId; + private final Map properties = new HashMap<>(); + + /** + * Creates a new {@link UpdateRequest}, which can be used to update the attributes of the + * of the tenant identified by the specified tenant ID. + * + *

      This method allows updating attributes of a tenant account, without first having to call + * {@link TenantManager#getTenant(String)}. + * + * @param tenantId a non-null, non-empty tenant ID string. + * @throws IllegalArgumentException If the tenant ID is null or empty. + */ + public UpdateRequest(String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "tenant ID must not be null or empty"); + this.tenantId = tenantId; + } + + String getTenantId() { + return tenantId; + } + + /** + * Sets the display name of the existing tenant. + * + * @param displayName a non-null, non-empty display name string. + */ + public UpdateRequest setDisplayName(String displayName) { + checkArgument(!Strings.isNullOrEmpty(displayName), "display name must not be null or empty"); + properties.put("displayName", displayName); + return this; + } + + /** + * Sets whether to allow email/password user authentication. + * + * @param passwordSignInAllowed a boolean indicating whether users can be authenticated using + * an email and password. + */ + public UpdateRequest setPasswordSignInAllowed(boolean passwordSignInAllowed) { + properties.put("allowPasswordSignup", passwordSignInAllowed); + return this; + } + + /** + * Sets whether to enable email link user authentication. + * + * @param emailLinkSignInEnabled a boolean indicating whether users can be authenticated using + * an email link. + */ + public UpdateRequest setEmailLinkSignInEnabled(boolean emailLinkSignInEnabled) { + properties.put("enableEmailLinkSignin", emailLinkSignInEnabled); + return this; + } + + Map getProperties() { + return ImmutableMap.copyOf(properties); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java new file mode 100644 index 000000000..540437404 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; +import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.AbstractFirebaseAuth; + +/** + * The tenant-aware Firebase client. + * + *

      This can be used to perform a variety of authentication-related operations, scoped to a + * particular tenant. + */ +public final class TenantAwareFirebaseAuth extends AbstractFirebaseAuth { + + private final String tenantId; + + TenantAwareFirebaseAuth(final FirebaseApp firebaseApp, final String tenantId) { + super(builderFromAppAndTenantId(firebaseApp, tenantId)); + checkArgument(!Strings.isNullOrEmpty(tenantId)); + this.tenantId = tenantId; + } + + /** Returns the client's tenant ID. */ + public String getTenantId() { + return tenantId; + } + + @Override + protected void doDestroy() { + // Nothing extra needs to be destroyed. + } +} diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java new file mode 100644 index 000000000..11f26b096 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java @@ -0,0 +1,287 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.HttpResponseInterceptor; +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.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.multitenancy.ListTenantsPage.DefaultTenantSource; +import com.google.firebase.auth.multitenancy.ListTenantsPage.PageFactory; +import com.google.firebase.auth.multitenancy.ListTenantsPage.TenantSource; +import com.google.firebase.auth.multitenancy.Tenant.CreateRequest; +import com.google.firebase.auth.multitenancy.Tenant.UpdateRequest; +import com.google.firebase.internal.CallableOperation; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import java.util.HashMap; +import java.util.Map; + +/** + * This class can be used to perform a variety of tenant-related operations, including creating, + * updating, and listing tenants. + */ +public final class TenantManager { + + private final FirebaseApp firebaseApp; + private final FirebaseTenantClient tenantClient; + private final Map tenantAwareAuths; + + /** + * Creates a new {@link TenantManager} instance. For internal use only. Use + * {@link FirebaseAuth#getTenantManager()} to obtain an instance for regular use. + * + * @hide + */ + public TenantManager(FirebaseApp firebaseApp) { + this.firebaseApp = firebaseApp; + this.tenantClient = new FirebaseTenantClient(firebaseApp); + this.tenantAwareAuths = new HashMap<>(); + } + + @VisibleForTesting + void setInterceptor(HttpResponseInterceptor interceptor) { + this.tenantClient.setInterceptor(interceptor); + } + + /** + * Gets the tenant corresponding to the specified tenant ID. + * + * @param tenantId A tenant ID string. + * @return A {@link Tenant} instance. + * @throws IllegalArgumentException If the tenant ID string is null or empty. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public Tenant getTenant(@NonNull String tenantId) throws FirebaseAuthException { + return getTenantOp(tenantId).call(); + } + + public synchronized TenantAwareFirebaseAuth getAuthForTenant(@NonNull String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + if (!tenantAwareAuths.containsKey(tenantId)) { + tenantAwareAuths.put(tenantId, new TenantAwareFirebaseAuth(firebaseApp, tenantId)); + } + return tenantAwareAuths.get(tenantId); + } + + /** + * Similar to {@link #getTenant(String)} but performs the operation asynchronously. + * + * @param tenantId A tenantId string. + * @return An {@code ApiFuture} which will complete successfully with a {@link Tenant} instance + * If an error occurs while retrieving tenant data or if the specified tenant ID does not + * exist, the future throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the tenant ID string is null or empty. + */ + public ApiFuture getTenantAsync(@NonNull String tenantId) { + return getTenantOp(tenantId).callAsync(firebaseApp); + } + + private CallableOperation getTenantOp(final String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + return new CallableOperation() { + @Override + protected Tenant execute() throws FirebaseAuthException { + return tenantClient.getTenant(tenantId); + } + }; + } + + /** + * Gets a page of tenants starting from the specified {@code pageToken}. Page size will be limited + * to 1000 tenants. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of tenants. + * @return A {@link ListTenantsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty. + * @throws FirebaseAuthException If an error occurs while retrieving tenant data. + */ + public ListTenantsPage listTenants(@Nullable String pageToken) throws FirebaseAuthException { + return listTenants(pageToken, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS); + } + + /** + * Gets a page of tenants starting from the specified {@code pageToken}. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of tenants. + * @param maxResults Maximum number of tenants to include in the returned page. This may not + * exceed 1000. + * @return A {@link ListTenantsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + * @throws FirebaseAuthException If an error occurs while retrieving tenant data. + */ + public ListTenantsPage listTenants(@Nullable String pageToken, int maxResults) + throws FirebaseAuthException { + return listTenantsOp(pageToken, maxResults).call(); + } + + /** + * Similar to {@link #listTenants(String)} but performs the operation asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of tenants. + * @return An {@code ApiFuture} which will complete successfully with a {@link ListTenantsPage} + * instance. If an error occurs while retrieving tenant data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty. + */ + public ApiFuture listTenantsAsync(@Nullable String pageToken) { + return listTenantsAsync(pageToken, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS); + } + + /** + * Similar to {@link #listTenants(String, int)} but performs the operation asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of tenants. + * @param maxResults Maximum number of tenants to include in the returned page. This may not + * exceed 1000. + * @return An {@code ApiFuture} which will complete successfully with a {@link ListTenantsPage} + * instance. If an error occurs while retrieving tenant data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + */ + public ApiFuture listTenantsAsync(@Nullable String pageToken, int maxResults) { + return listTenantsOp(pageToken, maxResults).callAsync(firebaseApp); + } + + private CallableOperation listTenantsOp( + @Nullable final String pageToken, final int maxResults) { + final TenantSource tenantSource = new DefaultTenantSource(tenantClient); + final PageFactory factory = new PageFactory(tenantSource, maxResults, pageToken); + return new CallableOperation() { + @Override + protected ListTenantsPage execute() throws FirebaseAuthException { + return factory.create(); + } + }; + } + + /** + * Creates a new tenant with the attributes contained in the specified {@link CreateRequest}. + * + * @param request A non-null {@link CreateRequest} instance. + * @return A {@link Tenant} instance corresponding to the newly created tenant. + * @throws NullPointerException if the provided request is null. + * @throws FirebaseAuthException if an error occurs while creating the tenant. + */ + public Tenant createTenant(@NonNull CreateRequest request) throws FirebaseAuthException { + return createTenantOp(request).call(); + } + + /** + * Similar to {@link #createTenant(CreateRequest)} but performs the operation asynchronously. + * + * @param request A non-null {@link CreateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link Tenant} + * instance corresponding to the newly created tenant. If an error occurs while creating the + * tenant, the future throws a {@link FirebaseAuthException}. + * @throws NullPointerException if the provided request is null. + */ + public ApiFuture createTenantAsync(@NonNull CreateRequest request) { + return createTenantOp(request).callAsync(firebaseApp); + } + + private CallableOperation createTenantOp( + final CreateRequest request) { + checkNotNull(request, "Create request must not be null."); + return new CallableOperation() { + @Override + protected Tenant execute() throws FirebaseAuthException { + return tenantClient.createTenant(request); + } + }; + } + + + /** + * Updates an existing user account with the attributes contained in the specified {@link + * UpdateRequest}. + * + * @param request A non-null {@link UpdateRequest} instance. + * @return A {@link Tenant} instance corresponding to the updated user account. + * @throws NullPointerException if the provided update request is null. + * @throws FirebaseAuthException if an error occurs while updating the user account. + */ + public Tenant updateTenant(@NonNull UpdateRequest request) throws FirebaseAuthException { + return updateTenantOp(request).call(); + } + + /** + * Similar to {@link #updateTenant(UpdateRequest)} but performs the operation asynchronously. + * + * @param request A non-null {@link UpdateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link Tenant} + * instance corresponding to the updated user account. If an error occurs while updating the + * user account, the future throws a {@link FirebaseAuthException}. + */ + public ApiFuture updateTenantAsync(@NonNull UpdateRequest request) { + return updateTenantOp(request).callAsync(firebaseApp); + } + + private CallableOperation updateTenantOp( + final UpdateRequest request) { + checkNotNull(request, "Update request must not be null."); + checkArgument(!request.getProperties().isEmpty(), + "Tenant update must have at least one property set."); + return new CallableOperation() { + @Override + protected Tenant execute() throws FirebaseAuthException { + return tenantClient.updateTenant(request); + } + }; + } + + /** + * Deletes the tenant identified by the specified tenant ID. + * + * @param tenantId A tenant ID string. + * @throws IllegalArgumentException If the tenant ID string is null or empty. + * @throws FirebaseAuthException If an error occurs while deleting the tenant. + */ + public void deleteTenant(@NonNull String tenantId) throws FirebaseAuthException { + deleteTenantOp(tenantId).call(); + } + + /** + * Similar to {@link #deleteTenant(String)} but performs the operation asynchronously. + * + * @param tenantId A tenant ID string. + * @return An {@code ApiFuture} which will complete successfully when the specified tenant account + * has been deleted. If an error occurs while deleting the tenant account, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the tenant ID string is null or empty. + */ + public ApiFuture deleteTenantAsync(String tenantId) { + return deleteTenantOp(tenantId).callAsync(firebaseApp); + } + + private CallableOperation deleteTenantOp(final String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + tenantClient.deleteTenant(tenantId); + return null; + } + }; + } +} diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 2915d1ddd..803591d81 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -46,9 +46,12 @@ 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.ProviderConfigTestUtils.TemporaryProviderConfig; +import com.google.firebase.auth.UserTestUtils.RandomUser; +import com.google.firebase.auth.UserTestUtils.TemporaryUser; import com.google.firebase.auth.hash.Scrypt; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.internal.Nullable; import com.google.firebase.testing.IntegrationTestUtils; import java.io.IOException; import java.net.URLDecoder; @@ -56,15 +59,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Random; -import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; public class FirebaseAuthIT { @@ -81,13 +82,12 @@ public class FirebaseAuthIT { private static final HttpTransport transport = Utils.getDefaultTransport(); private static final String ACTION_LINK_CONTINUE_URL = "http://localhost/?a=1&b=2#c=3"; - private static FirebaseAuth auth; + private static final FirebaseAuth auth = FirebaseAuth.getInstance( + IntegrationTestUtils.ensureDefaultApp()); - @BeforeClass - public static void setUpClass() { - FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp(); - auth = FirebaseAuth.getInstance(masterApp); - } + @Rule public final TemporaryUser temporaryUser = new TemporaryUser(auth); + @Rule public final TemporaryProviderConfig temporaryProviderConfig = + new TemporaryProviderConfig(auth); @Test public void testGetNonExistingUser() throws Exception { @@ -96,7 +96,7 @@ public void testGetNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -108,7 +108,7 @@ public void testGetNonExistingUserByEmail() throws Exception { fail("No error thrown for non existing email"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -116,11 +116,11 @@ public void testGetNonExistingUserByEmail() throws Exception { @Test public void testUpdateNonExistingUser() throws Exception { try { - auth.updateUserAsync(new UpdateRequest("non.existing")).get(); + auth.updateUserAsync(new UserRecord.UpdateRequest("non.existing")).get(); fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -132,7 +132,7 @@ public void testDeleteNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -211,50 +211,46 @@ private ApiFuture slowDeleteUsersAsync(List uids) thr @Test public void testCreateUserWithParams() throws Exception { - RandomUser randomUser = RandomUser.create(); - String phone = randomPhoneNumber(); - CreateRequest user = new CreateRequest() - .setUid(randomUser.uid) - .setEmail(randomUser.email) - .setPhoneNumber(phone) + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest() + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) .setDisplayName("Random User") .setPhotoUrl("https://example.com/photo.png") .setEmailVerified(true) .setPassword("password"); - UserRecord userRecord = auth.createUserAsync(user).get(); - try { - assertEquals(randomUser.uid, userRecord.getUid()); - assertEquals("Random User", userRecord.getDisplayName()); - assertEquals(randomUser.email, userRecord.getEmail()); - assertEquals(phone, userRecord.getPhoneNumber()); - assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); - assertTrue(userRecord.isEmailVerified()); - assertFalse(userRecord.isDisabled()); - - assertEquals(2, userRecord.getProviderData().length); - List providers = new ArrayList<>(); - for (UserInfo provider : userRecord.getProviderData()) { - providers.add(provider.getProviderId()); - } - assertTrue(providers.contains("password")); - assertTrue(providers.contains("phone")); + UserRecord userRecord = temporaryUser.create(user); + assertEquals(randomUser.getUid(), userRecord.getUid()); + assertEquals("Random User", userRecord.getDisplayName()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); + assertEquals(randomUser.getPhoneNumber(), userRecord.getPhoneNumber()); + assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); + assertTrue(userRecord.isEmailVerified()); + assertFalse(userRecord.isDisabled()); - checkRecreate(randomUser.uid); - } finally { - auth.deleteUserAsync(userRecord.getUid()).get(); + assertEquals(2, userRecord.getProviderData().length); + List providers = new ArrayList<>(); + for (UserInfo provider : userRecord.getProviderData()) { + providers.add(provider.getProviderId()); } + assertTrue(providers.contains("password")); + assertTrue(providers.contains("phone")); + + checkRecreateUser(randomUser.getUid()); } @Test public void testUserLifecycle() throws Exception { // Create user - UserRecord userRecord = auth.createUserAsync(new CreateRequest()).get(); + UserRecord userRecord = auth.createUserAsync(new UserRecord.CreateRequest()).get(); String uid = userRecord.getUid(); // Get user userRecord = auth.getUserAsync(userRecord.getUid()).get(); assertEquals(uid, userRecord.getUid()); + assertNull(userRecord.getTenantId()); assertNull(userRecord.getDisplayName()); assertNull(userRecord.getEmail()); assertNull(userRecord.getPhoneNumber()); @@ -267,20 +263,20 @@ public void testUserLifecycle() throws Exception { assertTrue(userRecord.getCustomClaims().isEmpty()); // Update user - RandomUser randomUser = RandomUser.create(); - String phone = randomPhoneNumber(); - UpdateRequest request = userRecord.updateRequest() + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + UserRecord.UpdateRequest request = userRecord.updateRequest() .setDisplayName("Updated Name") - .setEmail(randomUser.email) - .setPhoneNumber(phone) + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) .setPhotoUrl("https://example.com/photo.png") .setEmailVerified(true) .setPassword("secret"); userRecord = auth.updateUserAsync(request).get(); assertEquals(uid, userRecord.getUid()); + assertNull(userRecord.getTenantId()); assertEquals("Updated Name", userRecord.getDisplayName()); - assertEquals(randomUser.email, userRecord.getEmail()); - assertEquals(phone, userRecord.getPhoneNumber()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); + assertEquals(randomUser.getPhoneNumber(), userRecord.getPhoneNumber()); assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); assertTrue(userRecord.isEmailVerified()); assertFalse(userRecord.isDisabled()); @@ -299,8 +295,9 @@ public void testUserLifecycle() throws Exception { .setDisabled(true); userRecord = auth.updateUserAsync(request).get(); assertEquals(uid, userRecord.getUid()); + assertNull(userRecord.getTenantId()); assertNull(userRecord.getDisplayName()); - assertEquals(randomUser.email, userRecord.getEmail()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); assertNull(userRecord.getPhoneNumber()); assertNull(userRecord.getPhotoUrl()); assertTrue(userRecord.isEmailVerified()); @@ -310,22 +307,15 @@ public void testUserLifecycle() throws Exception { // Delete user auth.deleteUserAsync(userRecord.getUid()).get(); - try { - auth.getUserAsync(userRecord.getUid()).get(); - fail("No error thrown for deleted user"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); - } + UserTestUtils.assertUserDoesNotExist(auth, userRecord.getUid()); } @Test public void testLastRefreshTime() throws Exception { - RandomUser user = RandomUser.create(); - UserRecord newUserRecord = auth.createUser(new CreateRequest() - .setUid(user.uid) - .setEmail(user.email) + RandomUser user = UserTestUtils.generateRandomUserInfo(); + UserRecord newUserRecord = auth.createUser(new UserRecord.CreateRequest() + .setUid(user.getUid()) + .setEmail(user.getEmail()) .setEmailVerified(false) .setPassword("password")); @@ -366,110 +356,105 @@ public void testLastRefreshTime() throws Exception { public void testListUsers() throws Exception { final List uids = new ArrayList<>(); - try { - uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid()); - uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid()); - uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid()); - - // Test list by batches - final AtomicInteger collected = new AtomicInteger(0); - ListUsersPage page = auth.listUsersAsync(null).get(); - while (page != null) { - for (ExportedUserRecord user : page.getValues()) { - if (uids.contains(user.getUid())) { - collected.incrementAndGet(); - assertNotNull("Missing passwordHash field. A common cause would be " - + "forgetting to add the \"Firebase Authentication Admin\" permission. See " - + "instructions in CONTRIBUTING.md", user.getPasswordHash()); - assertNotNull(user.getPasswordSalt()); - } - } - page = page.getNextPage(); - } - assertEquals(uids.size(), collected.get()); + for (int i = 0; i < 3; i++) { + UserRecord.CreateRequest createRequest = + new UserRecord.CreateRequest().setPassword("password"); + uids.add(temporaryUser.create(createRequest).getUid()); + } - // Test iterate all - collected.set(0); - page = auth.listUsersAsync(null).get(); - for (ExportedUserRecord user : page.iterateAll()) { + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListUsersPage page = auth.listUsersAsync(null).get(); + while (page != null) { + for (ExportedUserRecord user : page.getValues()) { if (uids.contains(user.getUid())) { collected.incrementAndGet(); - assertNotNull(user.getPasswordHash()); + assertNotNull("Missing passwordHash field. A common cause would be " + + "forgetting to add the \"Firebase Authentication Admin\" permission. See " + + "instructions in CONTRIBUTING.md", user.getPasswordHash()); assertNotNull(user.getPasswordSalt()); + assertNull(user.getTenantId()); } } - assertEquals(uids.size(), collected.get()); - - // Test iterate async - collected.set(0); - final Semaphore semaphore = new Semaphore(0); - final AtomicReference error = new AtomicReference<>(); - ApiFuture pageFuture = auth.listUsersAsync(null); - ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { - @Override - public void onFailure(Throwable t) { - error.set(t); - semaphore.release(); - } + page = page.getNextPage(); + } + assertEquals(uids.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = auth.listUsersAsync(null).get(); + for (ExportedUserRecord user : page.iterateAll()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull(user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertNull(user.getTenantId()); + } + } + assertEquals(uids.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference error = new AtomicReference<>(); + ApiFuture pageFuture = auth.listUsersAsync(null); + ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } - @Override - public void onSuccess(ListUsersPage result) { - for (ExportedUserRecord user : result.iterateAll()) { - if (uids.contains(user.getUid())) { - collected.incrementAndGet(); - assertNotNull(user.getPasswordHash()); - assertNotNull(user.getPasswordSalt()); - } + @Override + public void onSuccess(ListUsersPage result) { + for (ExportedUserRecord user : result.iterateAll()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull(user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertNull(user.getTenantId()); } - semaphore.release(); } - }, MoreExecutors.directExecutor()); - semaphore.acquire(); - assertEquals(uids.size(), collected.get()); - assertNull(error.get()); - } finally { - for (String uid : uids) { - auth.deleteUserAsync(uid).get(); + semaphore.release(); } - } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(uids.size(), collected.get()); + assertNull(error.get()); } @Test public void testCustomClaims() throws Exception { - UserRecord userRecord = auth.createUserAsync(new CreateRequest()).get(); + UserRecord userRecord = temporaryUser.create(new UserRecord.CreateRequest()); String uid = userRecord.getUid(); - try { - // New user should not have any claims - assertTrue(userRecord.getCustomClaims().isEmpty()); - - Map expected = ImmutableMap.of( - "admin", true, "package", "gold"); - auth.setCustomUserClaimsAsync(uid, expected).get(); - - // Should have 2 claims - UserRecord updatedUser = auth.getUserAsync(uid).get(); - assertEquals(2, updatedUser.getCustomClaims().size()); - for (Map.Entry entry : expected.entrySet()) { - assertEquals(entry.getValue(), updatedUser.getCustomClaims().get(entry.getKey())); - } + // New user should not have any claims + assertTrue(userRecord.getCustomClaims().isEmpty()); - // User's ID token should have the custom claims - String customToken = auth.createCustomTokenAsync(uid).get(); - String idToken = signInWithCustomToken(customToken); - FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get(); - Map result = decoded.getClaims(); - for (Map.Entry entry : expected.entrySet()) { - assertEquals(entry.getValue(), result.get(entry.getKey())); - } + Map expected = ImmutableMap.of( + "admin", true, "package", "gold"); + auth.setCustomUserClaimsAsync(uid, expected).get(); - // Should be able to remove custom claims - auth.setCustomUserClaimsAsync(uid, null).get(); - updatedUser = auth.getUserAsync(uid).get(); - assertTrue(updatedUser.getCustomClaims().isEmpty()); - } finally { - auth.deleteUserAsync(uid).get(); + // Should have 2 claims + UserRecord updatedUser = auth.getUserAsync(uid).get(); + assertEquals(2, updatedUser.getCustomClaims().size()); + for (Map.Entry entry : expected.entrySet()) { + assertEquals(entry.getValue(), updatedUser.getCustomClaims().get(entry.getKey())); } + + // User's ID token should have the custom claims + String customToken = auth.createCustomTokenAsync(uid).get(); + String idToken = signInWithCustomToken(customToken); + FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get(); + Map result = decoded.getClaims(); + for (Map.Entry entry : expected.entrySet()) { + assertEquals(entry.getValue(), result.get(entry.getKey())); + } + + // Should be able to remove custom claims + auth.setCustomUserClaimsAsync(uid, null).get(); + updatedUser = auth.getUserAsync(uid).get(); + assertTrue(updatedUser.getCustomClaims().isEmpty()); } @Test @@ -527,7 +512,7 @@ public void testVerifyIdToken() throws Exception { } idToken = signInWithCustomToken(customToken); decoded = auth.verifyIdTokenAsync(idToken, true).get(); - assertEquals("user2", decoded.getUid()); + assertEquals("user2", decoded.getUid()); auth.deleteUserAsync("user2"); } @@ -581,32 +566,29 @@ public void testCustomTokenWithClaims() throws Exception { @Test public void testImportUsers() throws Exception { - RandomUser randomUser = RandomUser.create(); + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); ImportUserRecord user = ImportUserRecord.builder() - .setUid(randomUser.uid) - .setEmail(randomUser.email) + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) .build(); UserImportResult result = auth.importUsersAsync(ImmutableList.of(user)).get(); + temporaryUser.registerUid(randomUser.getUid()); assertEquals(1, result.getSuccessCount()); assertEquals(0, result.getFailureCount()); - try { - UserRecord savedUser = auth.getUserAsync(randomUser.uid).get(); - assertEquals(randomUser.email, savedUser.getEmail()); - } finally { - auth.deleteUserAsync(randomUser.uid).get(); - } + UserRecord savedUser = auth.getUserAsync(randomUser.getUid()).get(); + assertEquals(randomUser.getEmail(), savedUser.getEmail()); } @Test public void testImportUsersWithPassword() throws Exception { - RandomUser randomUser = RandomUser.create(); + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); final byte[] passwordHash = BaseEncoding.base64().decode( "V358E8LdWJXAO7muq0CufVpEOXaj8aFiC7T/rcaGieN04q/ZPJ08WhJEHGjj9lz/2TT+/86N5VjVoc5DdBhBiw=="); ImportUserRecord user = ImportUserRecord.builder() - .setUid(randomUser.uid) - .setEmail(randomUser.email) + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) .setPasswordHash(passwordHash) .setPasswordSalt("NaCl".getBytes()) .build(); @@ -622,88 +604,309 @@ public void testImportUsersWithPassword() throws Exception { .setRounds(8) .setMemoryCost(14) .build())).get(); + temporaryUser.registerUid(randomUser.getUid()); assertEquals(1, result.getSuccessCount()); assertEquals(0, result.getFailureCount()); - try { - 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(randomUser.uid).get(); - } + UserRecord savedUser = auth.getUserAsync(randomUser.getUid()).get(); + assertEquals(randomUser.getEmail(), savedUser.getEmail()); + String idToken = signInWithPassword(randomUser.getEmail(), "password"); + assertFalse(Strings.isNullOrEmpty(idToken)); } @Test public void testGeneratePasswordResetLink() throws Exception { - RandomUser user = RandomUser.create(); - auth.createUser(new CreateRequest() - .setUid(user.uid) - .setEmail(user.email) + RandomUser user = UserTestUtils.generateRandomUserInfo(); + temporaryUser.create(new UserRecord.CreateRequest() + .setUid(user.getUid()) + .setEmail(user.getEmail()) .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); - } + String link = auth.generatePasswordResetLink(user.getEmail(), 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.getEmail(), "password", "newpassword", + linkParams.get("oobCode")); + assertEquals(user.getEmail(), email); + // Password reset also verifies the user's email + assertTrue(auth.getUser(user.getUid()).isEmailVerified()); } @Test public void testGenerateEmailVerificationResetLink() throws Exception { - RandomUser user = RandomUser.create(); - auth.createUser(new CreateRequest() - .setUid(user.uid) - .setEmail(user.email) + RandomUser user = UserTestUtils.generateRandomUserInfo(); + temporaryUser.create(new UserRecord.CreateRequest() + .setUid(user.getUid()) + .setEmail(user.getEmail()) .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); - } + String link = auth.generateEmailVerificationLink(user.getEmail(), 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")); } @Test public void testGenerateSignInWithEmailLink() throws Exception { - RandomUser user = RandomUser.create(); - auth.createUser(new CreateRequest() - .setUid(user.uid) - .setEmail(user.email) + RandomUser user = UserTestUtils.generateRandomUserInfo(); + temporaryUser.create(new UserRecord.CreateRequest() + .setUid(user.getUid()) + .setEmail(user.getEmail()) .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); + String link = auth.generateSignInWithEmailLink(user.getEmail(), 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.getEmail(), linkParams.get("oobCode")); + assertFalse(Strings.isNullOrEmpty(idToken)); + assertTrue(auth.getUser(user.getUid()).isEmailVerified()); + } + + @Test + public void testOidcProviderConfigLifecycle() throws Exception { + // Create provider config + String providerId = "oidc.provider-id"; + OidcProviderConfig config = temporaryProviderConfig.createOidcProviderConfig( + new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setDisplayName("DisplayName") + .setEnabled(true) + .setClientId("ClientId") + .setIssuer("https://oidc.com/issuer")); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // Get provider config + config = auth.getOidcProviderConfigAsync(providerId).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // Update provider config + OidcProviderConfig.UpdateRequest updateRequest = + new OidcProviderConfig.UpdateRequest(providerId) + .setDisplayName("NewDisplayName") + .setEnabled(false) + .setClientId("NewClientId") + .setIssuer("https://oidc.com/new-issuer"); + config = auth.updateOidcProviderConfigAsync(updateRequest).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("NewDisplayName", config.getDisplayName()); + assertFalse(config.isEnabled()); + assertEquals("NewClientId", config.getClientId()); + assertEquals("https://oidc.com/new-issuer", config.getIssuer()); + + // Delete provider config + temporaryProviderConfig.deleteOidcProviderConfig(providerId); + ProviderConfigTestUtils.assertOidcProviderConfigDoesNotExist(auth, providerId); + } + + @Test + public void testListOidcProviderConfigs() throws Exception { + final List providerIds = new ArrayList<>(); + + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "oidc.provider-id" + i; + providerIds.add(providerId); + temporaryProviderConfig.createOidcProviderConfig( + new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer")); + } + + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListProviderConfigsPage page = + auth.listOidcProviderConfigsAsync(null).get(); + while (page != null) { + for (OidcProviderConfig providerConfig : page.getValues()) { + if (checkOidcProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + page = page.getNextPage(); + } + assertEquals(providerIds.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = auth.listOidcProviderConfigsAsync(null).get(); + for (OidcProviderConfig providerConfig : page.iterateAll()) { + if (checkOidcProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + assertEquals(providerIds.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference error = new AtomicReference<>(); + ApiFuture> pageFuture = + auth.listOidcProviderConfigsAsync(null); + ApiFutures.addCallback( + pageFuture, + new ApiFutureCallback>() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } + + @Override + public void onSuccess(ListProviderConfigsPage result) { + for (OidcProviderConfig providerConfig : result.iterateAll()) { + if (checkOidcProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + semaphore.release(); + } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(providerIds.size(), collected.get()); + assertNull(error.get()); + } + + @Test + public void testSamlProviderConfigLifecycle() throws Exception { + // Create provider config + String providerId = "saml.provider-id"; + SamlProviderConfig config = temporaryProviderConfig.createSamlProviderConfig( + new SamlProviderConfig.CreateRequest() + .setProviderId(providerId) + .setDisplayName("DisplayName") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler")); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + + config = auth.getSamlProviderConfig(providerId); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + + // Update provider config + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest(providerId) + .setDisplayName("NewDisplayName") + .setEnabled(false) + .addX509Certificate("certificate"); + config = auth.updateSamlProviderConfigAsync(updateRequest).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("NewDisplayName", config.getDisplayName()); + assertFalse(config.isEnabled()); + assertEquals(ImmutableList.of("certificate"), config.getX509Certificates()); + + // Delete provider config + temporaryProviderConfig.deleteSamlProviderConfig(providerId); + ProviderConfigTestUtils.assertSamlProviderConfigDoesNotExist(auth, providerId); + } + + @Test + public void testListSamlProviderConfigs() throws Exception { + final List providerIds = new ArrayList<>(); + + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "saml.provider-id" + i; + providerIds.add(providerId); + temporaryProviderConfig.createSamlProviderConfig( + new SamlProviderConfig.CreateRequest() + .setProviderId(providerId) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler")); + } + + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListProviderConfigsPage page = + auth.listSamlProviderConfigsAsync(null).get(); + while (page != null) { + for (SamlProviderConfig providerConfig : page.getValues()) { + if (checkSamlProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + page = page.getNextPage(); + } + assertEquals(providerIds.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = auth.listSamlProviderConfigsAsync(null).get(); + for (SamlProviderConfig providerConfig : page.iterateAll()) { + if (checkSamlProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } } + assertEquals(providerIds.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference error = new AtomicReference<>(); + ApiFuture> pageFuture = + auth.listSamlProviderConfigsAsync(null); + ApiFutures.addCallback( + pageFuture, + new ApiFutureCallback>() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } + + @Override + public void onSuccess(ListProviderConfigsPage result) { + for (SamlProviderConfig providerConfig : result.iterateAll()) { + if (checkSamlProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + semaphore.release(); + } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(providerIds.size(), collected.get()); + assertNull(error.get()); } private Map parseLinkParameters(String link) throws Exception { @@ -721,22 +924,22 @@ private Map parseLinkParameters(String link) throws Exception { return result; } - static 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 { + return signInWithCustomToken(customToken, null); } - private String signInWithCustomToken(String customToken) throws IOException { - GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key=" + private String signInWithCustomToken( + String customToken, @Nullable String tenantId) throws IOException { + final GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key=" + IntegrationTestUtils.getApiKey()); - Map content = ImmutableMap.of( - "token", customToken, "returnSecureToken", true); + ImmutableMap.Builder content = ImmutableMap.builder(); + content.put("token", customToken); + content.put("returnSecureToken", true); + if (tenantId != null) { + content.put("tenantId", tenantId); + } HttpRequest request = transport.createRequestFactory().buildPostRequest(url, - new JsonHttpContent(jsonFactory, content)); + new JsonHttpContent(jsonFactory, content.build())); request.setParser(new JsonObjectParser(jsonFactory)); HttpResponse response = request.execute(); try { @@ -800,9 +1003,9 @@ private String signInWithEmailLink( } } - private void checkRecreate(String uid) throws Exception { + private void checkRecreateUser(String uid) throws Exception { try { - auth.createUserAsync(new CreateRequest().setUid(uid)).get(); + auth.createUserAsync(new UserRecord.CreateRequest().setUid(uid)).get(); fail("No error thrown for creating user with existing ID"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); @@ -810,21 +1013,25 @@ private void checkRecreate(String uid) throws Exception { } } - static class RandomUser { - final String uid; - final String email; - - private RandomUser(String uid, String email) { - this.uid = uid; - this.email = email; + private boolean checkOidcProviderConfig(List providerIds, OidcProviderConfig config) { + if (providerIds.contains(config.getProviderId())) { + assertEquals("CLIENT_ID", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + return true; } + return false; + } - 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); + private boolean checkSamlProviderConfig(List providerIds, SamlProviderConfig config) { + if (providerIds.contains(config.getProviderId())) { + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + return true; } + return false; } static UserRecord newUserWithParams() throws Exception { @@ -834,13 +1041,14 @@ static UserRecord newUserWithParams() throws Exception { static UserRecord newUserWithParams(FirebaseAuth auth) throws Exception { // TODO(rsgowman): This function could be used throughout this file (similar to the other // ports). - RandomUser randomUser = RandomUser.create(); - return auth.createUser(new CreateRequest() - .setUid(randomUser.uid) - .setEmail(randomUser.email) - .setPhoneNumber(randomPhoneNumber()) + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + return auth.createUser(new UserRecord.CreateRequest() + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) .setDisplayName("Random User") .setPhotoUrl("https://example.com/photo.png") .setPassword("password")); } } + diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index 1bc05174f..635e1bfba 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -32,7 +32,6 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; -import com.google.firebase.auth.internal.FirebaseTokenFactory; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; import java.lang.reflect.InvocationTargetException; @@ -429,12 +428,12 @@ private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVe private FirebaseAuth getAuthForIdTokenVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); - FirebaseUserManager userManager = new FirebaseUserManager(app); - return FirebaseAuth.builder() - .setFirebaseApp(app) - .setIdTokenVerifier(tokenVerifierSupplier) - .setUserManager(Suppliers.ofInstance(userManager)) - .build(); + FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); + return new FirebaseAuth( + AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setIdTokenVerifier(tokenVerifierSupplier) + .setUserManager(Suppliers.ofInstance(userManager))); } private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) { @@ -444,12 +443,12 @@ private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier t private FirebaseAuth getAuthForSessionCookieVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); - FirebaseUserManager userManager = new FirebaseUserManager(app); - return FirebaseAuth.builder() - .setFirebaseApp(app) - .setCookieVerifier(tokenVerifierSupplier) - .setUserManager(Suppliers.ofInstance(userManager)) - .build(); + FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); + return new FirebaseAuth( + AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setCookieVerifier(tokenVerifierSupplier) + .setUserManager(Suppliers.ofInstance(userManager))); } private static class MockTokenVerifier implements FirebaseTokenVerifier { diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java index b10817afb..5dd1e9c14 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -29,6 +29,7 @@ import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.firebase.testing.ServiceAccount; import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -216,6 +217,50 @@ public void testMalformedToken() throws Exception { tokenVerifier.verifyToken("not.a.jwt"); } + @Test + public void testVerifyTokenDifferentTenantIds() { + try { + fullyPopulatedBuilder() + .setTenantId("TENANT_1") + .build() + .verifyToken(createTokenWithTenantId("TENANT_2")); + } catch (FirebaseAuthException e) { + assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals( + "The tenant ID ('TENANT_2') of the token did not match the expected value ('TENANT_1')", + e.getMessage()); + } + } + + @Test + public void testVerifyTokenMissingTenantId() { + try { + fullyPopulatedBuilder() + .setTenantId("TENANT_ID") + .build() + .verifyToken(tokenFactory.createToken()); + } catch (FirebaseAuthException e) { + assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals( + "The tenant ID ('') of the token did not match the expected value ('TENANT_ID')", + e.getMessage()); + } + } + + @Test + public void testVerifyTokenUnexpectedTenantId() { + try { + fullyPopulatedBuilder() + .build() + .verifyToken(createTokenWithTenantId("TENANT_ID")); + } catch (FirebaseAuthException e) { + assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals( + "The tenant ID ('TENANT_ID') of the token did not match the expected value ('')", + e.getMessage()); + } + } + @Test(expected = NullPointerException.class) public void testBuilderNoPublicKeysManager() { fullyPopulatedBuilder().setPublicKeysManager(null).build(); @@ -337,4 +382,10 @@ private String createTokenWithTimestamps(long issuedAtSeconds, long expirationSe payload.setExpirationTimeSeconds(expirationSeconds); return tokenFactory.createToken(payload); } + + private String createTokenWithTenantId(String tenantId) { + Payload payload = tokenFactory.createTokenPayload(); + payload.set("firebase", ImmutableMap.of("tenant", tenantId)); + return tokenFactory.createToken(payload); + } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 67d0448e9..d407fd8ec 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -16,10 +16,12 @@ package com.google.firebase.auth; +import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -42,10 +44,9 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; -import com.google.firebase.auth.UidIdentifier; -import com.google.firebase.auth.UserIdentifier; -import com.google.firebase.auth.UserRecord.CreateRequest; -import com.google.firebase.auth.UserRecord.UpdateRequest; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.auth.multitenancy.TenantAwareFirebaseAuth; +import com.google.firebase.auth.multitenancy.TenantManager; import com.google.firebase.internal.SdkUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; import com.google.firebase.testing.TestResponseInterceptor; @@ -67,8 +68,12 @@ public class FirebaseUserManagerTest { + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + 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) @@ -78,9 +83,15 @@ public class FirebaseUserManagerTest { .setAndroidInstallApp(true) .setAndroidMinimumVersion("6") .build(); + private static final Map ACTION_CODE_SETTINGS_MAP = ACTION_CODE_SETTINGS.getProperties(); + private static final String PROJECT_BASE_URL = + "https://identitytoolkit.googleapis.com/v2/projects/test-project-id"; + + private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants"; + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -120,9 +131,9 @@ public void testGetUserWithNotFoundError() throws Exception { FirebaseAuth.getInstance().getUserAsync("testuser").get(); fail("No error thrown for invalid response"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } @@ -143,9 +154,9 @@ public void testGetUserByEmailWithNotFoundError() throws Exception { FirebaseAuth.getInstance().getUserByEmailAsync("testuser@example.com").get(); fail("No error thrown for invalid response"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } @@ -166,9 +177,9 @@ public void testGetUserByPhoneNumberWithNotFoundError() throws Exception { FirebaseAuth.getInstance().getUserByPhoneNumberAsync("+1234567890").get(); fail("No error thrown for invalid response"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } @@ -371,7 +382,8 @@ public void testCreateUser() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement( TestUtils.loadResource("createUser.json"), TestUtils.loadResource("getUser.json")); - UserRecord user = FirebaseAuth.getInstance().createUserAsync(new CreateRequest()).get(); + UserRecord user = + FirebaseAuth.getInstance().createUserAsync(new UserRecord.CreateRequest()).get(); checkUserRecord(user); checkRequestHeaders(interceptor); } @@ -382,7 +394,7 @@ public void testUpdateUser() throws Exception { TestUtils.loadResource("createUser.json"), TestUtils.loadResource("getUser.json")); UserRecord user = FirebaseAuth.getInstance() - .updateUserAsync(new UpdateRequest("testuser")).get(); + .updateUserAsync(new UserRecord.UpdateRequest("testuser")).get(); checkUserRecord(user); checkRequestHeaders(interceptor); } @@ -397,12 +409,9 @@ public void testSetCustomAttributes() throws Exception { FirebaseAuth.getInstance().setCustomUserClaimsAsync("testuser", claims).get(); 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); + GenericJson parsed = parseRequestContent(interceptor); assertEquals("testuser", parsed.get("localId")); - assertEquals(jsonFactory.toString(claims), parsed.get("customAttributes")); + assertEquals(JSON_FACTORY.toString(claims), parsed.get("customAttributes")); } @Test @@ -413,10 +422,7 @@ public void testRevokeRefreshTokens() throws Exception { FirebaseAuth.getInstance().revokeRefreshTokensAsync("testuser").get(); 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); + GenericJson parsed = parseRequestContent(interceptor); assertEquals("testuser", parsed.get("localId")); assertNotNull(parsed.get("validSince")); } @@ -518,14 +524,11 @@ public void testImportUsers() throws Exception { assertEquals(0, result.getFailureCount()); assertTrue(result.getErrors().isEmpty()); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(1, parsed.size()); List> expected = ImmutableList.of( - user1.getProperties(jsonFactory), - user2.getProperties(jsonFactory) + user1.getProperties(JSON_FACTORY), + user2.getProperties(JSON_FACTORY) ); assertEquals(expected, parsed.get("users")); } @@ -558,15 +561,12 @@ public void testImportUsersError() throws Exception { assertEquals(2, error.getIndex()); assertEquals("Another error occurred in user3", error.getReason()); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(1, parsed.size()); List> expected = ImmutableList.of( - user1.getProperties(jsonFactory), - user2.getProperties(jsonFactory), - user3.getProperties(jsonFactory) + user1.getProperties(JSON_FACTORY), + user2.getProperties(JSON_FACTORY), + user3.getProperties(JSON_FACTORY) ); assertEquals(expected, parsed.get("users")); } @@ -596,14 +596,11 @@ protected Map getOptions() { assertEquals(0, result.getFailureCount()); assertTrue(result.getErrors().isEmpty()); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(4, parsed.size()); List> expected = ImmutableList.of( - user1.getProperties(jsonFactory), - user2.getProperties(jsonFactory) + user1.getProperties(JSON_FACTORY), + user2.getProperties(JSON_FACTORY) ); assertEquals(expected, parsed.get("users")); assertEquals("MOCK_HASH", parsed.get("hashAlgorithm")); @@ -669,10 +666,7 @@ public void testCreateSessionCookie() throws Exception { assertEquals("MockCookieString", cookie); 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); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(2, parsed.size()); assertEquals("testToken", parsed.get("idToken")); assertEquals(new BigDecimal(3600), parsed.get("validDuration")); @@ -754,13 +748,13 @@ public void call(FirebaseAuth auth) throws Exception { .add(new UserManagerOp() { @Override public void call(FirebaseAuth auth) throws Exception { - auth.createUserAsync(new CreateRequest()).get(); + auth.createUserAsync(new UserRecord.CreateRequest()).get(); } }) .add(new UserManagerOp() { @Override public void call(FirebaseAuth auth) throws Exception { - auth.updateUserAsync(new UpdateRequest("test")).get(); + auth.updateUserAsync(new UserRecord.UpdateRequest("test")).get(); } }) .add(new UserManagerOp() { @@ -790,12 +784,12 @@ public void call(FirebaseAuth auth) throws Exception { operation.call(auth); fail("No error thrown for HTTP error: " + code); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); String msg = String.format("Unexpected HTTP response with status: %d; body: {}", code); assertEquals(msg, authException.getMessage()); - assertTrue(authException.getCause() instanceof HttpResponseException); - assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode()); + assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); + assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); } } } @@ -808,11 +802,11 @@ public void call(FirebaseAuth auth) throws Exception { operation.call(auth); fail("No error thrown for HTTP error"); } catch (ExecutionException e) { - assertTrue(e.getCause().toString(), e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause().toString(), e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("User management service responded with an error", authException.getMessage()); - assertTrue(authException.getCause() instanceof HttpResponseException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals("Firebase Auth service responded with an error", authException.getMessage()); + assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } } @@ -824,10 +818,10 @@ public void testGetUserMalformedJsonError() throws Exception { FirebaseAuth.getInstance().getUserAsync("testuser").get(); fail("No error thrown for JSON error"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertTrue(authException.getCause() instanceof IOException); - assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode()); + assertThat(authException.getCause(), instanceOf(IOException.class)); + assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); } } @@ -841,12 +835,12 @@ public void testGetUserUnexpectedHttpError() throws Exception { auth.getUserAsync("testuser").get(); fail("No error thrown for JSON error"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertTrue(authException.getCause() instanceof HttpResponseException); + assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); assertEquals("Unexpected HTTP response with status: 500; body: {\"not\" json}", authException.getMessage()); - assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); } } @@ -874,13 +868,13 @@ public void testTimeout() throws Exception { @Test public void testUserBuilder() { - Map map = new CreateRequest().getProperties(); + Map map = new UserRecord.CreateRequest().getProperties(); assertTrue(map.isEmpty()); } @Test public void testUserBuilderWithParams() { - Map map = new CreateRequest() + Map map = new UserRecord.CreateRequest() .setUid("TestUid") .setDisplayName("Display Name") .setPhotoUrl("http://test.com/example.png") @@ -901,7 +895,7 @@ public void testUserBuilderWithParams() { @Test public void testInvalidUid() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setUid(null); fail("No error thrown for null uid"); @@ -926,7 +920,7 @@ public void testInvalidUid() { @Test public void testInvalidDisplayName() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setDisplayName(null); fail("No error thrown for null display name"); @@ -937,7 +931,7 @@ public void testInvalidDisplayName() { @Test public void testInvalidPhotoUrl() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setPhotoUrl(null); fail("No error thrown for null photo url"); @@ -962,7 +956,7 @@ public void testInvalidPhotoUrl() { @Test public void testInvalidEmail() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setEmail(null); fail("No error thrown for null email"); @@ -987,7 +981,7 @@ public void testInvalidEmail() { @Test public void testInvalidPhoneNumber() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setPhoneNumber(null); fail("No error thrown for null phone number"); @@ -1012,7 +1006,7 @@ public void testInvalidPhoneNumber() { @Test public void testInvalidPassword() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setPassword(null); fail("No error thrown for null password"); @@ -1030,7 +1024,7 @@ public void testInvalidPassword() { @Test public void testUserUpdater() throws IOException { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); Map claims = ImmutableMap.of("admin", true, "package", "gold"); Map map = update .setDisplayName("Display Name") @@ -1040,7 +1034,7 @@ public void testUserUpdater() throws IOException { .setEmailVerified(true) .setPassword("secret") .setCustomClaims(claims) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(8, map.size()); assertEquals(update.getUid(), map.get("localId")); assertEquals("Display Name", map.get("displayName")); @@ -1049,12 +1043,12 @@ public void testUserUpdater() throws IOException { assertEquals("+1234567890", map.get("phoneNumber")); assertTrue((Boolean) map.get("emailVerified")); assertEquals("secret", map.get("password")); - assertEquals(Utils.getDefaultJsonFactory().toString(claims), map.get("customAttributes")); + assertEquals(JSON_FACTORY.toString(claims), map.get("customAttributes")); } @Test public void testNullJsonFactory() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); Map claims = ImmutableMap.of("admin", true, "package", "gold"); update.setCustomClaims(claims); try { @@ -1067,10 +1061,10 @@ public void testNullJsonFactory() { @Test public void testNullCustomClaims() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); Map map = update .setCustomClaims(null) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(2, map.size()); assertEquals(update.getUid(), map.get("localId")); assertEquals("{}", map.get("customAttributes")); @@ -1078,10 +1072,10 @@ public void testNullCustomClaims() { @Test public void testEmptyCustomClaims() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); Map map = update .setCustomClaims(ImmutableMap.of()) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(2, map.size()); assertEquals(update.getUid(), map.get("localId")); assertEquals("{}", map.get("customAttributes")); @@ -1089,31 +1083,31 @@ public void testEmptyCustomClaims() { @Test public void testDeleteDisplayName() { - Map map = new UpdateRequest("test") + Map map = new UserRecord.UpdateRequest("test") .setDisplayName(null) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(ImmutableList.of("DISPLAY_NAME"), map.get("deleteAttribute")); } @Test public void testDeletePhotoUrl() { - Map map = new UpdateRequest("test") + Map map = new UserRecord.UpdateRequest("test") .setPhotoUrl(null) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(ImmutableList.of("PHOTO_URL"), map.get("deleteAttribute")); } @Test public void testDeletePhoneNumber() { - Map map = new UpdateRequest("test") + Map map = new UserRecord.UpdateRequest("test") .setPhoneNumber(null) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(ImmutableList.of("phone"), map.get("deleteProvider")); } @Test public void testInvalidUpdatePhotoUrl() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); try { update.setPhotoUrl(""); fail("No error thrown for invalid photo url"); @@ -1131,7 +1125,7 @@ public void testInvalidUpdatePhotoUrl() { @Test public void testInvalidUpdateEmail() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); try { update.setEmail(null); fail("No error thrown for null email"); @@ -1156,7 +1150,7 @@ public void testInvalidUpdateEmail() { @Test public void testInvalidUpdatePhoneNumber() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); try { update.setPhoneNumber(""); @@ -1175,7 +1169,7 @@ public void testInvalidUpdatePhoneNumber() { @Test public void testInvalidUpdatePassword() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); try { update.setPassword(null); fail("No error thrown for null password"); @@ -1193,7 +1187,7 @@ public void testInvalidUpdatePassword() { @Test public void testInvalidCustomClaims() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); for (String claim : FirebaseUserManager.RESERVED_CLAIMS) { try { update.setCustomClaims(ImmutableMap.of(claim, "value")); @@ -1210,10 +1204,10 @@ public void testLargeCustomClaims() { for (int i = 0; i < 1001; i++) { builder.append("a"); } - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); update.setCustomClaims(ImmutableMap.of("key", builder.toString())); try { - update.getProperties(Utils.getDefaultJsonFactory()); + update.getProperties(JSON_FACTORY); fail("No error thrown for large claims payload"); } catch (Exception ignore) { // expected @@ -1245,10 +1239,7 @@ public void testGeneratePasswordResetLinkWithSettings() throws Exception { 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); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3 + ACTION_CODE_SETTINGS_MAP.size(), parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("PASSWORD_RESET", parsed.get("requestType")); @@ -1267,10 +1258,7 @@ public void testGeneratePasswordResetLink() throws Exception { 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); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3, parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("PASSWORD_RESET", parsed.get("requestType")); @@ -1302,10 +1290,7 @@ public void testGenerateEmailVerificationLinkWithSettings() throws Exception { 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); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3 + ACTION_CODE_SETTINGS_MAP.size(), parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("VERIFY_EMAIL", parsed.get("requestType")); @@ -1324,10 +1309,7 @@ public void testGenerateEmailVerificationLink() throws Exception { 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); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3, parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("VERIFY_EMAIL", parsed.get("requestType")); @@ -1372,10 +1354,7 @@ public void testGenerateSignInWithEmailLinkWithSettings() throws Exception { 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); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3 + ACTION_CODE_SETTINGS_MAP.size(), parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("EMAIL_SIGNIN", parsed.get("requestType")); @@ -1397,7 +1376,7 @@ public void testHttpErrorWithCode() { fail("No exception thrown for HTTP error"); } catch (FirebaseAuthException e) { assertEquals("unauthorized-continue-uri", e.getErrorCode()); - assertTrue(e.getCause() instanceof HttpResponseException); + assertThat(e.getCause(), instanceOf(HttpResponseException.class)); } } @@ -1413,11 +1392,1188 @@ public void testUnexpectedHttpError() { fail("No exception thrown for HTTP error"); } catch (FirebaseAuthException e) { assertEquals("internal-error", e.getErrorCode()); - assertTrue(e.getCause() instanceof HttpResponseException); + assertThat(e.getCause(), instanceOf(HttpResponseException.class)); + } + } + + @Test + public void testCreateOidcProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("oidc.provider-id", url.getFirst("oauthIdpConfigId")); + } + + @Test + public void testCreateOidcProviderAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = + FirebaseAuth.getInstance().createOidcProviderConfigAsync(createRequest).get(); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("oidc.provider-id", url.getFirst("oauthIdpConfigId")); + } + + @Test + public void testCreateOidcProviderMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + // Only the 'enabled' and 'displayName' fields can be omitted from an OIDC provider config + // creation request. + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId("oidc.provider-id") + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericJson parsed = parseRequestContent(interceptor); + assertNull(parsed.get("displayName")); + assertNull(parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("oidc.provider-id", url.getFirst("oauthIdpConfigId")); + } + + @Test + public void testCreateOidcProviderError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest().setProviderId("oidc.provider-id"); + try { + FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); + } + + @Test + public void testCreateOidcProviderMissingId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + try { + FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + fail("No error thrown for invalid response"); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testTenantAwareCreateOidcProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.createOidcProviderConfig(createRequest); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs"); + } + + @Test + public void testUpdateOidcProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = FirebaseAuth.getInstance().updateOidcProviderConfig(request); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("clientId,displayName,enabled,issuer", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + } + + @Test + public void testUpdateOidcProviderAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = + FirebaseAuth.getInstance().updateOidcProviderConfigAsync(request).get(); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("clientId,displayName,enabled,issuer", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + } + + @Test + public void testUpdateOidcProviderMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME"); + + OidcProviderConfig config = FirebaseAuth.getInstance().updateOidcProviderConfig(request); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("displayName", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(1, parsed.size()); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + } + + @Test + public void testUpdateOidcProviderConfigNoValues() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + try { + FirebaseAuth.getInstance().updateOidcProviderConfig( + new OidcProviderConfig.UpdateRequest("oidc.provider-id")); + fail("No error thrown for empty provider config update"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testUpdateOidcProviderConfigError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME"); + try { + FirebaseAuth.getInstance().updateOidcProviderConfig(request); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testTenantAwareUpdateOidcProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("oidc.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = tenantAwareAuth.updateOidcProviderConfig(request); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id"; + checkUrl(interceptor, "PATCH", expectedUrl); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("clientId,displayName,enabled,issuer", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + } + + @Test + public void testGetOidcProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + + OidcProviderConfig config = + FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id"); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testGetOidcProviderConfigAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + + OidcProviderConfig config = + FirebaseAuth.getInstance().getOidcProviderConfigAsync("oidc.provider-id").get(); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testGetOidcProviderConfigMissingId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + + try { + FirebaseAuth.getInstance().getOidcProviderConfig(null); + fail("No error thrown for missing provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testGetOidcProviderConfigInvalidId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + + try { + FirebaseAuth.getInstance().getOidcProviderConfig("saml.invalid-oidc-provider-id"); + fail("No error thrown for invalid provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testGetOidcProviderConfigWithNotFoundError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testGetTenantAwareOidcProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("oidc.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + OidcProviderConfig config = tenantAwareAuth.getOidcProviderConfig("oidc.provider-id"); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testListOidcProviderConfigs() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listOidc.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listOidcProviderConfigs(null, 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1"); + checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testListOidcProviderConfigsAsync() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listOidc.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listOidcProviderConfigsAsync(null, 99).get(); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1"); + checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testListOidcProviderConfigsError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + + try { + FirebaseAuth.getInstance().listOidcProviderConfigs(null, 99); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); + } + + @Test + public void testListOidcProviderConfigsWithPageToken() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listOidc.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listOidcProviderConfigs("token", 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1"); + checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertEquals("token", url.getFirst("nextPageToken")); + } + + @Test + public void testListZeroOidcProviderConfigs() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listOidcProviderConfigs(null); + assertTrue(Iterables.isEmpty(page.getValues())); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + } + + @Test + public void testTenantAwareListOidcProviderConfigs() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("listOidc.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + ListProviderConfigsPage page = + tenantAwareAuth.listOidcProviderConfigs(null, 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1"); + checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testDeleteOidcProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.provider-id"); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testDeleteOidcProviderConfigAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + FirebaseAuth.getInstance().deleteOidcProviderConfigAsync("oidc.provider-id").get(); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testDeleteOidcProviderMissingId() throws Exception { + initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteOidcProviderConfig(null); + fail("No error thrown for missing provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteOidcProviderInvalidId() throws Exception { + initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteOidcProviderConfig("saml.invalid-oidc-provider-id"); + fail("No error thrown for invalid provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteOidcProviderConfigWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); } + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.UNKNOWN"); } - private static TestResponseInterceptor initializeAppForUserManagement(String ...responses) { + @Test + public void testTenantAwareDeleteOidcProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + "{}"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.deleteOidcProviderConfig("oidc.provider-id"); + + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id"; + checkUrl(interceptor, "DELETE", expectedUrl); + } + + @Test + public void testCreateSamlProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + SamlProviderConfig config = FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("saml.provider-id", url.getFirst("inboundSamlConfigId")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testCreateSamlProviderAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + SamlProviderConfig config = + FirebaseAuth.getInstance().createSamlProviderConfigAsync(createRequest).get(); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("saml.provider-id", url.getFirst("inboundSamlConfigId")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testCreateSamlProviderMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + // Only the 'enabled', 'displayName', and 'signRequest' fields can be omitted from a SAML + // provider config creation request. + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("saml.provider-id", url.getFirst("inboundSamlConfigId")); + + GenericJson parsed = parseRequestContent(interceptor); + assertNull(parsed.get("displayName")); + assertNull(parsed.get("enabled")); + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(1, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate"), idpCertificates.get(0)); + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testCreateSamlProviderError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest().setProviderId("saml.provider-id"); + try { + FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); + } + + @Test + public void testCreateSamlProviderMissingId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + try { + FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + fail("No error thrown for invalid response"); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testTenantAwareCreateSamlProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("saml.json")); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.createSamlProviderConfig(createRequest); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs"); + } + + @Test + public void testUpdateSamlProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + SamlProviderConfig config = FirebaseAuth.getInstance().updateSamlProviderConfig(updateRequest); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals( + "displayName,enabled,idpConfig.idpCertificates,idpConfig.idpEntityId,idpConfig.ssoUrl," + + "spConfig.callbackUri,spConfig.spEntityId", + url.getFirst("updateMask")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testUpdateSamlProviderAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + SamlProviderConfig config = + FirebaseAuth.getInstance().updateSamlProviderConfigAsync(updateRequest).get(); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals( + "displayName,enabled,idpConfig.idpCertificates,idpConfig.idpEntityId,idpConfig.ssoUrl," + + "spConfig.callbackUri,spConfig.spEntityId", + url.getFirst("updateMask")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testUpdateSamlProviderMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.UpdateRequest request = + new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME"); + + SamlProviderConfig config = FirebaseAuth.getInstance().updateSamlProviderConfig(request); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("displayName", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(1, parsed.size()); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + } + + @Test + public void testUpdateSamlProviderConfigNoValues() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + try { + FirebaseAuth.getInstance().updateSamlProviderConfig( + new SamlProviderConfig.UpdateRequest("saml.provider-id")); + fail("No error thrown for empty provider config update"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testUpdateSamlProviderConfigError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + SamlProviderConfig.UpdateRequest request = + new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME"); + try { + FirebaseAuth.getInstance().updateSamlProviderConfig(request); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testTenantAwareUpdateSamlProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("saml.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login"); + + SamlProviderConfig config = tenantAwareAuth.updateSamlProviderConfig(updateRequest); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id"; + checkUrl(interceptor, "PATCH", expectedUrl); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("displayName,enabled,idpConfig.idpEntityId,idpConfig.ssoUrl", + url.getFirst("updateMask")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(2, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + } + + @Test + public void testGetSamlProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + + SamlProviderConfig config = + FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id"); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testGetSamlProviderConfigAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + + SamlProviderConfig config = + FirebaseAuth.getInstance().getSamlProviderConfigAsync("saml.provider-id").get(); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testGetSamlProviderConfigMissingId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + + try { + FirebaseAuth.getInstance().getSamlProviderConfig(null); + fail("No error thrown for missing provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testGetSamlProviderConfigInvalidId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + + try { + FirebaseAuth.getInstance().getSamlProviderConfig("oidc.invalid-saml-provider-id"); + fail("No error thrown for invalid provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testGetSamlProviderConfigWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testGetTenantAwareSamlProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("saml.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + SamlProviderConfig config = tenantAwareAuth.getSamlProviderConfig("saml.provider-id"); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id"; + checkUrl(interceptor, "GET", expectedUrl); + } + + @Test + public void testListSamlProviderConfigs() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listSaml.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listSamlProviderConfigs(null, 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1"); + checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testListSamlProviderConfigsAsync() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listSaml.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listSamlProviderConfigsAsync(null, 99).get(); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1"); + checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testListSamlProviderConfigsError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + + try { + FirebaseAuth.getInstance().listSamlProviderConfigs(null, 99); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); + } + + @Test + public void testListSamlProviderConfigsWithPageToken() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listSaml.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listSamlProviderConfigs("token", 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1"); + checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertEquals("token", url.getFirst("nextPageToken")); + } + + @Test + public void testListZeroSamlProviderConfigs() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listSamlProviderConfigs(null); + assertTrue(Iterables.isEmpty(page.getValues())); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + } + + @Test + public void testTenantAwareListSamlProviderConfigs() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("listSaml.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + ListProviderConfigsPage page = + tenantAwareAuth.listSamlProviderConfigs(null, 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1"); + checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testDeleteSamlProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.provider-id"); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testDeleteSamlProviderConfigAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + FirebaseAuth.getInstance().deleteSamlProviderConfigAsync("saml.provider-id").get(); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testDeleteSamlProviderMissingId() throws Exception { + initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteSamlProviderConfig(null); + fail("No error thrown for missing provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteSamlProviderInvalidId() throws Exception { + initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteSamlProviderConfig("oidc.invalid-saml-provider-id"); + fail("No error thrown for invalid provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteSamlProviderConfigWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.UNKNOWN"); + } + + @Test + public void testTenantAwareDeleteSamlProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + "{}"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.deleteSamlProviderConfig("saml.provider-id"); + + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id"; + checkUrl(interceptor, "DELETE", expectedUrl); + } + + private static TestResponseInterceptor initializeAppForUserManagementWithStatusCode( + int statusCode, String response) { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport( + new MockHttpTransport.Builder().setLowLevelHttpResponse( + new MockLowLevelHttpResponse().setContent(response).setStatusCode(statusCode)).build()) + .setProjectId("test-project-id") + .build()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getUserManager().setInterceptor(interceptor); + return interceptor; + } + + private static TestResponseInterceptor initializeAppForTenantAwareUserManagement( + String tenantId, + String... responses) { + initializeAppWithResponses(responses); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + TenantManager tenantManager = FirebaseAuth.getInstance().getTenantManager(); + AbstractFirebaseAuth auth = tenantManager.getAuthForTenant(tenantId); + auth.getUserManager().setInterceptor(interceptor); + return interceptor; + } + + private static TestResponseInterceptor initializeAppForUserManagement(String... responses) { + initializeAppWithResponses(responses); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getUserManager().setInterceptor(interceptor); + return interceptor; + } + + private static void initializeAppWithResponses(String... responses) { List mocks = new ArrayList<>(); for (String response : responses) { mocks.add(new MockLowLevelHttpResponse().setContent(response)); @@ -1428,11 +2584,13 @@ private static TestResponseInterceptor initializeAppForUserManagement(String ... .setHttpTransport(transport) .setProjectId("test-project-id") .build()); - FirebaseAuth auth = FirebaseAuth.getInstance(); - FirebaseUserManager userManager = auth.getUserManager(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - userManager.setInterceptor(interceptor); - return interceptor; + } + + private static GenericJson parseRequestContent(TestResponseInterceptor interceptor) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + return JSON_FACTORY.fromString(new String(out.toByteArray()), GenericJson.class); } private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse response) { @@ -1444,15 +2602,19 @@ private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse respon .setProjectId("test-project-id") .setHttpTransport(transport) .build()); - return FirebaseAuth.builder() - .setFirebaseApp(app) - .setUserManager(new Supplier() { - @Override - public FirebaseUserManager get() { - return new FirebaseUserManager(app, transport.createRequestFactory()); - } - }) - .build(); + return new FirebaseAuth( + AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setUserManager(new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager + .builder() + .setFirebaseApp(app) + .setHttpRequestFactory(transport.createRequestFactory()) + .build(); + } + })); } private static void checkUserRecord(UserRecord userRecord) { @@ -1467,6 +2629,7 @@ private static void checkUserRecord(UserRecord userRecord) { assertFalse(userRecord.isDisabled()); assertTrue(userRecord.isEmailVerified()); assertEquals(1494364393000L, userRecord.getTokensValidAfterTimestamp()); + assertEquals("testTenant", userRecord.getTenantId()); UserInfo provider = userRecord.getProviderData()[0]; assertEquals("testuser@example.com", provider.getUid()); @@ -1488,6 +2651,25 @@ private static void checkUserRecord(UserRecord userRecord) { assertEquals("gold", claims.get("package")); } + private static void checkOidcProviderConfig(OidcProviderConfig config, String providerId) { + assertEquals(providerId, config.getProviderId()); + assertEquals("DISPLAY_NAME", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("CLIENT_ID", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + } + + private static void checkSamlProviderConfig(SamlProviderConfig config, String providerId) { + assertEquals(providerId, config.getProviderId()); + assertEquals("DISPLAY_NAME", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + } + private static void checkRequestHeaders(TestResponseInterceptor interceptor) { HttpHeaders headers = interceptor.getResponse().getRequest().getHeaders(); String auth = "Bearer " + TEST_TOKEN; @@ -1497,8 +2679,20 @@ private static void checkRequestHeaders(TestResponseInterceptor interceptor) { assertEquals(clientVersion, headers.getFirstHeaderStringValue("X-Client-Version")); } + private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) { + HttpRequest request = interceptor.getResponse().getRequest(); + if (method.equals("PATCH")) { + assertEquals("PATCH", + request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override")); + assertEquals("POST", request.getRequestMethod()); + } else { + assertEquals(method, request.getRequestMethod()); + } + assertEquals(url, request.getUrl().toString().split("\\?")[0]); + } + private interface UserManagerOp { void call(FirebaseAuth auth) throws Exception; } - + } diff --git a/src/test/java/com/google/firebase/auth/GetUsersIT.java b/src/test/java/com/google/firebase/auth/GetUsersIT.java index efe2f783f..a0bdcb6c6 100644 --- a/src/test/java/com/google/firebase/auth/GetUsersIT.java +++ b/src/test/java/com/google/firebase/auth/GetUsersIT.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.UserTestUtils.RandomUser; import com.google.firebase.testing.IntegrationTestUtils; import java.util.Collection; @@ -44,18 +45,17 @@ public static void setUpClass() throws Exception { testUser2 = FirebaseAuthIT.newUserWithParams(auth); testUser3 = FirebaseAuthIT.newUserWithParams(auth); - FirebaseAuthIT.RandomUser randomUser = FirebaseAuthIT.RandomUser.create(); - importUserUid = randomUser.uid; - String phone = FirebaseAuthIT.randomPhoneNumber(); + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + importUserUid = randomUser.getUid(); UserImportResult result = auth.importUsers(ImmutableList.of( ImportUserRecord.builder() - .setUid(randomUser.uid) - .setEmail(randomUser.email) - .setPhoneNumber(phone) + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) .addUserProvider( UserProvider.builder() .setProviderId("google.com") - .setUid("google_" + randomUser.uid) + .setUid("google_" + randomUser.getUid()) .build()) .build() )); diff --git a/src/test/java/com/google/firebase/auth/ListProviderConfigsPageTest.java b/src/test/java/com/google/firebase/auth/ListProviderConfigsPageTest.java new file mode 100644 index 000000000..ba08f9c77 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/ListProviderConfigsPageTest.java @@ -0,0 +1,376 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.junit.Test; + +public class ListProviderConfigsPageTest { + + @Test + public void testSinglePage() throws FirebaseAuthException, IOException { + TestProviderConfigSource source = new TestProviderConfigSource(3); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListProviderConfigsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(3, providerConfigs.size()); + for (int i = 0; i < 3; i++) { + assertEquals("oidc.provider-id-" + i, providerConfigs.get(i).getProviderId()); + } + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testMultiplePages() throws FirebaseAuthException, IOException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-0"), + newOidcProviderConfig("oidc.provider-id-1"), + newOidcProviderConfig("oidc.provider-id-2")), + "token"); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page1 = + new ListProviderConfigsPage.Factory(source).create(); + assertTrue(page1.hasNextPage()); + assertEquals("token", page1.getNextPageToken()); + ImmutableList providerConfigs = ImmutableList.copyOf(page1.getValues()); + assertEquals(3, providerConfigs.size()); + for (int i = 0; i < 3; i++) { + assertEquals("oidc.provider-id-" + i, providerConfigs.get(i).getProviderId()); + } + + response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-3"), + newOidcProviderConfig("oidc.provider-id-4"), + newOidcProviderConfig("oidc.provider-id-5")), + ListProviderConfigsPage.END_OF_LIST); + source.response = response; + ListProviderConfigsPage page2 = page1.getNextPage(); + assertFalse(page2.hasNextPage()); + assertEquals(ListProviderConfigsPage.END_OF_LIST, page2.getNextPageToken()); + providerConfigs = ImmutableList.copyOf(page2.getValues()); + assertEquals(3, providerConfigs.size()); + for (int i = 3; i < 6; i++) { + assertEquals("oidc.provider-id-" + i, providerConfigs.get(i - 3).getProviderId()); + } + + assertEquals(2, source.calls.size()); + assertNull(source.calls.get(0)); + assertEquals("token", source.calls.get(1)); + + // Should iterate all provider configs from both pages + int iterations = 0; + for (OidcProviderConfig providerConfig : page1.iterateAll()) { + iterations++; + } + assertEquals(6, iterations); + assertEquals(3, source.calls.size()); + assertEquals("token", source.calls.get(2)); + + // Should only iterate provider configs in the last page + iterations = 0; + for (OidcProviderConfig providerConfig : page2.iterateAll()) { + iterations++; + } + assertEquals(3, iterations); + assertEquals(3, source.calls.size()); + } + + @Test + public void testListProviderConfigsIterable() throws FirebaseAuthException, IOException { + TestProviderConfigSource source = new TestProviderConfigSource(3); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterable providerConfigs = page.iterateAll(); + + int iterations = 0; + for (OidcProviderConfig providerConfig : providerConfigs) { + assertEquals("oidc.provider-id-" + iterations, providerConfig.getProviderId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + // Should result in a new iterator + iterations = 0; + for (OidcProviderConfig providerConfig : providerConfigs) { + assertEquals("oidc.provider-id-" + iterations, providerConfig.getProviderId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testListProviderConfigsIterator() throws FirebaseAuthException, IOException { + TestProviderConfigSource source = new TestProviderConfigSource(3); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterable providerConfigs = page.iterateAll(); + Iterator iterator = providerConfigs.iterator(); + int iterations = 0; + while (iterator.hasNext()) { + assertEquals("oidc.provider-id-" + iterations, iterator.next().getProviderId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + while (iterator.hasNext()) { + fail("Should not be able to to iterate any more"); + } + try { + iterator.next(); + fail("Should not be able to iterate any more"); + } catch (NoSuchElementException expected) { + // expected + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testListProviderConfigsPagedIterable() throws FirebaseAuthException, IOException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-0"), + newOidcProviderConfig("oidc.provider-id-1"), + newOidcProviderConfig("oidc.provider-id-2")), + "token"); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + int iterations = 0; + for (OidcProviderConfig providerConfig : page.iterateAll()) { + assertEquals("oidc.provider-id-" + iterations, providerConfig.getProviderId()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-3"), + newOidcProviderConfig("oidc.provider-id-4"), + newOidcProviderConfig("oidc.provider-id-5")), + ListProviderConfigsPage.END_OF_LIST); + source.response = response; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1)); + } + + @Test + public void testListProviderConfigsPagedIterator() throws FirebaseAuthException, IOException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-0"), + newOidcProviderConfig("oidc.provider-id-1"), + newOidcProviderConfig("oidc.provider-id-2")), + "token"); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterator providerConfigs = page.iterateAll().iterator(); + int iterations = 0; + while (providerConfigs.hasNext()) { + assertEquals("oidc.provider-id-" + iterations, providerConfigs.next().getProviderId()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-3"), + newOidcProviderConfig("oidc.provider-id-4"), + newOidcProviderConfig("oidc.provider-id-5")), + ListProviderConfigsPage.END_OF_LIST); + source.response = response; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1)); + assertFalse(providerConfigs.hasNext()); + try { + providerConfigs.next(); + } catch (NoSuchElementException e) { + // expected + } + } + + @Test + public void testPageWithNoproviderConfigs() throws FirebaseAuthException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of(), ListProviderConfigsPage.END_OF_LIST); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListProviderConfigsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + assertEquals(0, ImmutableList.copyOf(page.getValues()).size()); + assertEquals(1, source.calls.size()); + } + + @Test + public void testIterableWithNoproviderConfigs() throws FirebaseAuthException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of(), ListProviderConfigsPage.END_OF_LIST); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + for (OidcProviderConfig providerConfig : page.iterateAll()) { + fail("Should not be able to iterate, but got: " + providerConfig); + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testIteratorWithNoproviderConfigs() throws FirebaseAuthException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of(), ListProviderConfigsPage.END_OF_LIST); + TestProviderConfigSource source = new TestProviderConfigSource(response); + + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + while (iterator.hasNext()) { + fail("Should not be able to iterate"); + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testRemove() throws FirebaseAuthException, IOException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of(newOidcProviderConfig("oidc.provider-id-1")), + ListProviderConfigsPage.END_OF_LIST); + TestProviderConfigSource source = new TestProviderConfigSource(response); + + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + while (iterator.hasNext()) { + assertNotNull(iterator.next()); + try { + iterator.remove(); + } catch (UnsupportedOperationException expected) { + // expected + } + } + } + + @Test(expected = NullPointerException.class) + public void testNullSource() { + new ListProviderConfigsPage.Factory(null); + } + + @Test + public void testInvalidPageToken() throws IOException { + TestProviderConfigSource source = new TestProviderConfigSource(1); + try { + new ListProviderConfigsPage.Factory(source, 1000, ""); + fail("No error thrown for empty page token"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidMaxResults() throws IOException { + TestProviderConfigSource source = new TestProviderConfigSource(1); + try { + new ListProviderConfigsPage.Factory(source, 1001, ""); + fail("No error thrown for maxResult > 1000"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ListProviderConfigsPage.Factory(source, 0, "next"); + fail("No error thrown for maxResult = 0"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ListProviderConfigsPage.Factory(source, -1, "next"); + fail("No error thrown for maxResult < 0"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + private static OidcProviderConfig newOidcProviderConfig(String providerConfigId) + throws IOException { + return Utils.getDefaultJsonFactory().fromString( + String.format("{\"name\":\"%s\"}", providerConfigId), OidcProviderConfig.class); + } + + private static class TestProviderConfigSource + implements ListProviderConfigsPage.ProviderConfigSource { + + private ListOidcProviderConfigsResponse response; + private final List calls = new ArrayList<>(); + + TestProviderConfigSource(int providerConfigCount) throws IOException { + ImmutableList.Builder providerConfigs = ImmutableList.builder(); + for (int i = 0; i < providerConfigCount; i++) { + providerConfigs.add(newOidcProviderConfig("oidc.provider-id-" + i)); + } + this.response = new ListOidcProviderConfigsResponse( + providerConfigs.build(), ListProviderConfigsPage.END_OF_LIST); + } + + TestProviderConfigSource(ListOidcProviderConfigsResponse response) { + this.response = response; + } + + @Override + public ListOidcProviderConfigsResponse fetch(int maxResults, String pageToken) { + calls.add(pageToken); + return response; + } + } +} diff --git a/src/test/java/com/google/firebase/auth/ListUsersPageTest.java b/src/test/java/com/google/firebase/auth/ListUsersPageTest.java index fb4a7d275..5e848069d 100644 --- a/src/test/java/com/google/firebase/auth/ListUsersPageTest.java +++ b/src/test/java/com/google/firebase/auth/ListUsersPageTest.java @@ -27,6 +27,7 @@ import com.google.api.client.json.JsonFactory; import com.google.common.collect.ImmutableList; import com.google.common.io.BaseEncoding; +import com.google.firebase.auth.ListUsersPage; import com.google.firebase.auth.ListUsersPage.ListUsersResult; import com.google.firebase.auth.internal.DownloadAccountResponse; import java.io.IOException; @@ -45,7 +46,7 @@ public class ListUsersPageTest { @Test public void testSinglePage() throws FirebaseAuthException, IOException { TestUserSource source = new TestUserSource(3); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); assertFalse(page.hasNextPage()); assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken()); assertNull(page.getNextPage()); @@ -68,7 +69,7 @@ public void testRedactedPasswords() throws FirebaseAuthException, IOException { newUser("user2", REDACTED_BASE64)), ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); assertFalse(page.hasNextPage()); assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken()); assertNull(page.getNextPage()); @@ -89,7 +90,7 @@ public void testMultiplePages() throws FirebaseAuthException, IOException { ImmutableList.of(newUser("user0"), newUser("user1"), newUser("user2")), "token"); TestUserSource source = new TestUserSource(result); - ListUsersPage page1 = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page1 = new ListUsersPage.Factory(source).create(); assertTrue(page1.hasNextPage()); assertEquals("token", page1.getNextPageToken()); ImmutableList users = ImmutableList.copyOf(page1.getValues()); @@ -136,7 +137,7 @@ public void testMultiplePages() throws FirebaseAuthException, IOException { @Test public void testListUsersIterable() throws FirebaseAuthException, IOException { TestUserSource source = new TestUserSource(3); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterable users = page.iterateAll(); int iterations = 0; @@ -162,7 +163,7 @@ public void testListUsersIterable() throws FirebaseAuthException, IOException { @Test public void testListUsersIterator() throws FirebaseAuthException, IOException { TestUserSource source = new TestUserSource(3); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterable users = page.iterateAll(); Iterator iterator = users.iterator(); int iterations = 0; @@ -192,7 +193,7 @@ public void testListUsersPagedIterable() throws FirebaseAuthException, IOExcepti ImmutableList.of(newUser("user0"), newUser("user1"), newUser("user2")), "token"); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); int iterations = 0; for (ExportedUserRecord user : page.iterateAll()) { assertEquals("user" + iterations, user.getUid()); @@ -218,7 +219,7 @@ public void testListUsersPagedIterator() throws FirebaseAuthException, IOExcepti ImmutableList.of(newUser("user0"), newUser("user1"), newUser("user2")), "token"); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterator users = page.iterateAll().iterator(); int iterations = 0; while (users.hasNext()) { @@ -251,7 +252,7 @@ public void testPageWithNoUsers() throws FirebaseAuthException { ImmutableList.of(), ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); assertFalse(page.hasNextPage()); assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken()); assertNull(page.getNextPage()); @@ -265,7 +266,7 @@ public void testIterableWithNoUsers() throws FirebaseAuthException { ImmutableList.of(), ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); for (ExportedUserRecord user : page.iterateAll()) { fail("Should not be able to iterate, but got: " + user); } @@ -279,7 +280,7 @@ public void testIteratorWithNoUsers() throws FirebaseAuthException { ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterator iterator = page.iterateAll().iterator(); while (iterator.hasNext()) { fail("Should not be able to iterate"); @@ -294,7 +295,7 @@ public void testRemove() throws FirebaseAuthException, IOException { ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterator iterator = page.iterateAll().iterator(); while (iterator.hasNext()) { assertNotNull(iterator.next()); @@ -308,14 +309,14 @@ public void testRemove() throws FirebaseAuthException, IOException { @Test(expected = NullPointerException.class) public void testNullSource() { - new ListUsersPage.PageFactory(null); + new ListUsersPage.Factory(null); } @Test public void testInvalidPageToken() throws IOException { TestUserSource source = new TestUserSource(1); try { - new ListUsersPage.PageFactory(source, 1000, ""); + new ListUsersPage.Factory(source, 1000, ""); fail("No error thrown for empty page token"); } catch (IllegalArgumentException expected) { // expected @@ -326,21 +327,21 @@ public void testInvalidPageToken() throws IOException { public void testInvalidMaxResults() throws IOException { TestUserSource source = new TestUserSource(1); try { - new ListUsersPage.PageFactory(source, 1001, ""); + new ListUsersPage.Factory(source, 1001, ""); fail("No error thrown for maxResult > 1000"); } catch (IllegalArgumentException expected) { // expected } try { - new ListUsersPage.PageFactory(source, 0, "next"); + new ListUsersPage.Factory(source, 0, "next"); fail("No error thrown for maxResult = 0"); } catch (IllegalArgumentException expected) { // expected } try { - new ListUsersPage.PageFactory(source, -1, "next"); + new ListUsersPage.Factory(source, -1, "next"); fail("No error thrown for maxResult < 0"); } catch (IllegalArgumentException expected) { // expected diff --git a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java new file mode 100644 index 000000000..1fb4ca37b --- /dev/null +++ b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import java.io.IOException; +import java.util.Map; +import org.junit.Test; + +public class OidcProviderConfigTest { + + private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + + private static final String OIDC_JSON_STRING = + ("{" + + " 'name': 'projects/projectId/oauthIdpConfigs/oidc.provider-id'," + + " 'displayName': 'DISPLAY_NAME'," + + " 'enabled': true," + + " 'clientId': 'CLIENT_ID'," + + " 'issuer': 'https://oidc.com/issuer'" + + "}").replace("'", "\""); + + @Test + public void testJsonDeserialization() throws IOException { + OidcProviderConfig config = jsonFactory.fromString(OIDC_JSON_STRING, OidcProviderConfig.class); + + assertEquals("oidc.provider-id", config.getProviderId()); + assertEquals("DISPLAY_NAME", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("CLIENT_ID", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + } + + @Test + public void testCreateRequest() throws IOException { + OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest(); + createRequest + .setProviderId("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(false) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + assertEquals("oidc.provider-id", createRequest.getProviderId()); + Map properties = createRequest.getProperties(); + assertEquals(properties.size(), 4); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + assertEquals("CLIENT_ID", (String) properties.get("clientId")); + assertEquals("https://oidc.com/issuer", (String) properties.get("issuer")); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingProviderId() { + new OidcProviderConfig.CreateRequest().setProviderId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidProviderId() { + new OidcProviderConfig.CreateRequest().setProviderId("saml.provider-id"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingDisplayName() { + new OidcProviderConfig.CreateRequest().setDisplayName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingClientId() { + new OidcProviderConfig.CreateRequest().setClientId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingIssuer() { + new OidcProviderConfig.CreateRequest().setIssuer(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidIssuerUrl() { + new OidcProviderConfig.CreateRequest().setIssuer("not a valid url"); + } + + @Test + public void testUpdateRequestFromOidcProviderConfig() throws IOException { + OidcProviderConfig config = jsonFactory.fromString(OIDC_JSON_STRING, OidcProviderConfig.class); + + OidcProviderConfig.UpdateRequest updateRequest = config.updateRequest(); + + assertEquals("oidc.provider-id", updateRequest.getProviderId()); + assertTrue(updateRequest.getProperties().isEmpty()); + } + + @Test + public void testUpdateRequest() throws IOException { + OidcProviderConfig.UpdateRequest updateRequest = + new OidcProviderConfig.UpdateRequest("oidc.provider-id"); + updateRequest + .setDisplayName("DISPLAY_NAME") + .setEnabled(false) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + assertEquals("oidc.provider-id", updateRequest.getProviderId()); + Map properties = updateRequest.getProperties(); + assertEquals(properties.size(), 4); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + assertEquals("CLIENT_ID", (String) properties.get("clientId")); + assertEquals("https://oidc.com/issuer", (String) properties.get("issuer")); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingProviderId() { + new OidcProviderConfig.UpdateRequest(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidProviderId() { + new OidcProviderConfig.UpdateRequest("saml.provider-id"); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingDisplayName() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingClientId() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setClientId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingIssuer() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setIssuer(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidIssuerUrl() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setIssuer("not a valid url"); + } +} diff --git a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java new file mode 100644 index 000000000..c01ac6501 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java @@ -0,0 +1,125 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.firebase.auth.internal.AuthHttpClient; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.junit.rules.ExternalResource; + +public class ProviderConfigTestUtils { + + public static void assertOidcProviderConfigDoesNotExist( + AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception { + try { + firebaseAuth.getOidcProviderConfigAsync(providerId).get(); + fail("No error thrown for getting a deleted OIDC provider config."); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + public static void assertSamlProviderConfigDoesNotExist( + AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception { + try { + firebaseAuth.getSamlProviderConfigAsync(providerId).get(); + fail("No error thrown for getting a deleted SAML provider config."); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + /** + * Creates temporary provider configs for testing, and deletes them at the end of each test case. + */ + public static final class TemporaryProviderConfig extends ExternalResource { + + private final AbstractFirebaseAuth auth; + private final List oidcIds = new ArrayList<>(); + private final List samlIds = new ArrayList<>(); + + public TemporaryProviderConfig(AbstractFirebaseAuth auth) { + this.auth = auth; + } + + public synchronized OidcProviderConfig createOidcProviderConfig( + OidcProviderConfig.CreateRequest request) throws FirebaseAuthException { + OidcProviderConfig config = auth.createOidcProviderConfig(request); + oidcIds.add(config.getProviderId()); + return config; + } + + public synchronized void deleteOidcProviderConfig( + String providerId) throws FirebaseAuthException { + checkArgument(oidcIds.contains(providerId), + "Provider ID is not currently associated with a temporary OIDC provider config: " + + providerId); + auth.deleteOidcProviderConfig(providerId); + oidcIds.remove(providerId); + } + + public synchronized SamlProviderConfig createSamlProviderConfig( + SamlProviderConfig.CreateRequest request) throws FirebaseAuthException { + SamlProviderConfig config = auth.createSamlProviderConfig(request); + samlIds.add(config.getProviderId()); + return config; + } + + public synchronized void deleteSamlProviderConfig( + String providerId) throws FirebaseAuthException { + checkArgument(samlIds.contains(providerId), + "Provider ID is not currently associated with a temporary SAML provider config: " + + providerId); + auth.deleteSamlProviderConfig(providerId); + samlIds.remove(providerId); + } + + @Override + protected synchronized void after() { + // Delete OIDC provider configs. + for (String id : oidcIds) { + try { + auth.deleteOidcProviderConfig(id); + } catch (Exception ignore) { + // Ignore + } + } + oidcIds.clear(); + + // Delete SAML provider configs. + for (String id : samlIds) { + try { + auth.deleteSamlProviderConfig(id); + } catch (Exception ignore) { + // Ignore + } + } + samlIds.clear(); + } + } +} + diff --git a/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java b/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java new file mode 100644 index 000000000..e957c1ddb --- /dev/null +++ b/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java @@ -0,0 +1,322 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +public class SamlProviderConfigTest { + + private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + + private static final String SAML_JSON_STRING = + ("{" + + " 'name': 'projects/projectId/inboundSamlConfigs/saml.provider-id'," + + " 'displayName': 'DISPLAY_NAME'," + + " 'enabled': true," + + " 'idpConfig': {" + + " 'idpEntityId': 'IDP_ENTITY_ID'," + + " 'ssoUrl': 'https://example.com/login'," + + " 'idpCertificates': [" + + " { 'x509Certificate': 'certificate1' }," + + " { 'x509Certificate': 'certificate2' }" + + " ]" + + " }," + + " 'spConfig': {" + + " 'spEntityId': 'RP_ENTITY_ID'," + + " 'callbackUri': 'https://projectId.firebaseapp.com/__/auth/handler'" + + " }" + + "}").replace("'", "\""); + + @Test + public void testJsonDeserialization() throws IOException { + SamlProviderConfig config = jsonFactory.fromString(SAML_JSON_STRING, SamlProviderConfig.class); + + assertEquals("saml.provider-id", config.getProviderId()); + assertEquals("DISPLAY_NAME", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + } + + @Test + public void testCreateRequest() throws IOException { + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(false) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + assertEquals("saml.provider-id", createRequest.getProviderId()); + Map properties = createRequest.getProperties(); + assertEquals(4, properties.size()); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) properties.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testCreateRequestX509Certificates() throws IOException { + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .addX509Certificate("certificate1") + .addAllX509Certificates(ImmutableList.of("certificate2", "certificate3")) + .addX509Certificate("certificate4"); + + Map properties = createRequest.getProperties(); + assertEquals(1, properties.size()); + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(1, idpConfig.size()); + + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(4, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate3"), idpCertificates.get(2)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate4"), idpCertificates.get(3)); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingProviderId() { + new SamlProviderConfig.CreateRequest().setProviderId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidProviderId() { + new SamlProviderConfig.CreateRequest().setProviderId("oidc.provider-id"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingDisplayName() { + new SamlProviderConfig.CreateRequest().setDisplayName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingIdpEntityId() { + new SamlProviderConfig.CreateRequest().setIdpEntityId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingSsoUrl() { + new SamlProviderConfig.CreateRequest().setSsoUrl(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidSsoUrl() { + new SamlProviderConfig.CreateRequest().setSsoUrl("not a valid url"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingX509Certificate() { + new SamlProviderConfig.CreateRequest().addX509Certificate(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestNullX509CertificatesCollection() { + new SamlProviderConfig.CreateRequest().addAllX509Certificates(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestEmptyX509CertificatesCollection() { + new SamlProviderConfig.CreateRequest().addAllX509Certificates(ImmutableList.of()); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingRpEntityId() { + new SamlProviderConfig.CreateRequest().setRpEntityId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingCallbackUrl() { + new SamlProviderConfig.CreateRequest().setCallbackUrl(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidCallbackUrl() { + new SamlProviderConfig.CreateRequest().setCallbackUrl("not a valid url"); + } + + @Test + public void testUpdateRequestFromSamlProviderConfig() throws IOException { + SamlProviderConfig config = jsonFactory.fromString(SAML_JSON_STRING, SamlProviderConfig.class); + + SamlProviderConfig.UpdateRequest updateRequest = config.updateRequest(); + + assertEquals("saml.provider-id", updateRequest.getProviderId()); + assertTrue(updateRequest.getProperties().isEmpty()); + } + + @Test + public void testUpdateRequest() throws IOException { + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id"); + updateRequest + .setDisplayName("DISPLAY_NAME") + .setEnabled(false) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + Map properties = updateRequest.getProperties(); + assertEquals(4, properties.size()); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) properties.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testUpdateRequestX509Certificates() throws IOException { + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id"); + updateRequest + .addX509Certificate("certificate1") + .addAllX509Certificates(ImmutableList.of("certificate2", "certificate3")) + .addX509Certificate("certificate4"); + + Map properties = updateRequest.getProperties(); + assertEquals(1, properties.size()); + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(1, idpConfig.size()); + + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(4, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate3"), idpCertificates.get(2)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate4"), idpCertificates.get(3)); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingProviderId() { + new SamlProviderConfig.UpdateRequest(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidProviderId() { + new SamlProviderConfig.UpdateRequest("oidc.invalid-saml-provider-id"); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingDisplayName() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingIdpEntityId() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setIdpEntityId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingSsoUrl() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setSsoUrl(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidSsoUrl() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setSsoUrl("not a valid url"); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingX509Certificate() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").addX509Certificate(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestNullX509CertificatesCollection() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").addAllX509Certificates(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestEmptyX509CertificatesCollection() { + new SamlProviderConfig.UpdateRequest("saml.provider-id") + .addAllX509Certificates(ImmutableList.of()); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingRpEntityId() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setRpEntityId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingCallbackUrl() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setCallbackUrl(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidCallbackUrl() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setCallbackUrl("not a valid url"); + } +} diff --git a/src/test/java/com/google/firebase/auth/UserTestUtils.java b/src/test/java/com/google/firebase/auth/UserTestUtils.java new file mode 100644 index 000000000..aa86e6e19 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/UserTestUtils.java @@ -0,0 +1,124 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.firebase.auth.internal.AuthHttpClient; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import org.junit.rules.ExternalResource; + +public final class UserTestUtils { + + public static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, String uid) + throws Exception { + try { + firebaseAuth.getUserAsync(uid).get(); + fail("No error thrown for getting a user which was expected to be absent."); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + public static RandomUser generateRandomUserInfo() { + String uid = UUID.randomUUID().toString().replaceAll("-", ""); + String email = String.format( + "test%s@example.%s.com", + uid.substring(0, 12), + uid.substring(12)).toLowerCase(); + return new RandomUser(uid, email, generateRandomPhoneNumber()); + } + + private static String generateRandomPhoneNumber() { + Random random = new Random(); + StringBuilder builder = new StringBuilder("+1"); + for (int i = 0; i < 10; i++) { + builder.append(random.nextInt(10)); + } + return builder.toString(); + } + + public static class RandomUser { + private final String uid; + private final String email; + private final String phoneNumber; + + private RandomUser(String uid, String email, String phoneNumber) { + this.uid = uid; + this.email = email; + this.phoneNumber = phoneNumber; + } + + public String getUid() { + return uid; + } + + public String getEmail() { + return email; + } + + public String getPhoneNumber() { + return phoneNumber; + } + } + + /** + * Creates temporary Firebase user accounts for testing, and deletes them at the end of each + * test case. + */ + public static final class TemporaryUser extends ExternalResource { + + private final AbstractFirebaseAuth auth; + private final List users = new ArrayList<>(); + + public TemporaryUser(AbstractFirebaseAuth auth) { + this.auth = auth; + } + + public UserRecord create(UserRecord.CreateRequest request) throws FirebaseAuthException { + UserRecord user = auth.createUser(request); + registerUid(user.getUid()); + return user; + } + + public synchronized void registerUid(String uid) { + users.add(uid); + } + + @Override + protected synchronized void after() { + for (String uid : users) { + try { + auth.deleteUser(uid); + } catch (Exception ignore) { + // Ignore + } + } + + users.clear(); + } + } +} + 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 71b95bee8..1c85ceeee 100644 --- a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java +++ b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import com.google.api.client.json.GenericJson; @@ -45,6 +46,7 @@ public class FirebaseTokenFactoryTest { private static final String USER_ID = "fuber"; private static final GenericJson EXTRA_CLAIMS = new GenericJson(); private static final String ISSUER = "test-484@mg-test-1210.iam.gserviceaccount.com"; + private static final String TENANT_ID = "tenant-id"; static { EXTRA_CLAIMS.set("one", 2).set("three", "four").setFactory(FACTORY); @@ -71,6 +73,7 @@ public void checkSignatureForToken() throws Exception { assertEquals(USER_ID, signedJwt.getPayload().getUid()); assertEquals(2L, signedJwt.getPayload().getIssuedAtTimeSeconds().longValue()); assertTrue(TestUtils.verifySignature(signedJwt, ImmutableList.of(keys.getPublic()))); + assertNull(signedJwt.getPayload().getTenantId()); jwt = tokenFactory.createSignedCustomAuthTokenForUser(USER_ID); signedJwt = FirebaseCustomAuthToken.parse(FACTORY, jwt); @@ -80,6 +83,23 @@ public void checkSignatureForToken() throws Exception { assertEquals(USER_ID, signedJwt.getPayload().getUid()); assertEquals(2L, signedJwt.getPayload().getIssuedAtTimeSeconds().longValue()); assertTrue(TestUtils.verifySignature(signedJwt, ImmutableList.of(keys.getPublic()))); + assertNull(signedJwt.getPayload().getTenantId()); + } + + @Test + public void tokenWithTenantId() throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(512); + KeyPair keys = keyGen.genKeyPair(); + FixedClock clock = new FixedClock(2002L); + CryptoSigner cryptoSigner = new TestCryptoSigner(keys.getPrivate()); + FirebaseTokenFactory tokenFactory = + new FirebaseTokenFactory(FACTORY, clock, cryptoSigner, TENANT_ID); + + String jwt = tokenFactory.createSignedCustomAuthTokenForUser(USER_ID); + FirebaseCustomAuthToken signedJwt = FirebaseCustomAuthToken.parse(FACTORY, jwt); + + assertEquals(TENANT_ID, signedJwt.getPayload().getTenantId()); } @Test diff --git a/src/test/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponseTest.java b/src/test/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponseTest.java new file mode 100644 index 000000000..fbe587e56 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponseTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.OidcProviderConfig; + +import org.junit.Test; + +public class ListOidcProviderConfigsResponseTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + @Test + public void testDefaultValues() throws Exception { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse(); + + assertEquals(0, response.getProviderConfigs().size()); + assertFalse(response.hasProviderConfigs()); + assertEquals("", response.getPageToken()); + } + + @Test + public void testEmptyTenantList() throws Exception { + ListOidcProviderConfigsResponse response = + new ListOidcProviderConfigsResponse(ImmutableList.of(), "PAGE_TOKEN"); + + assertEquals(0, response.getProviderConfigs().size()); + assertFalse(response.hasProviderConfigs()); + } + + @Test + public void testDeserialization() throws Exception { + String json = JSON_FACTORY.toString( + ImmutableMap.of( + "oauthIdpConfigs", ImmutableList.of( + ImmutableMap.of("name", "projects/projectId/oauthIdpConfigs/oidc.provider-id-1"), + ImmutableMap.of("name", "projects/projectId/oauthIdpConfigs/oidc.provider-id-2")), + "nextPageToken", "PAGE_TOKEN")); + ListOidcProviderConfigsResponse response = + JSON_FACTORY.fromString(json, ListOidcProviderConfigsResponse.class); + + assertEquals(2, response.getProviderConfigs().size()); + assertEquals("oidc.provider-id-1", response.getProviderConfigs().get(0).getProviderId()); + assertEquals("oidc.provider-id-2", response.getProviderConfigs().get(1).getProviderId()); + assertTrue(response.hasProviderConfigs()); + assertEquals("PAGE_TOKEN", response.getPageToken()); + } +} diff --git a/src/test/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponseTest.java b/src/test/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponseTest.java new file mode 100644 index 000000000..6950fe352 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponseTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.SamlProviderConfig; + +import org.junit.Test; + +public class ListSamlProviderConfigsResponseTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + @Test + public void testDefaultValues() throws Exception { + ListSamlProviderConfigsResponse response = new ListSamlProviderConfigsResponse(); + + assertEquals(0, response.getProviderConfigs().size()); + assertFalse(response.hasProviderConfigs()); + assertEquals("", response.getPageToken()); + } + + @Test + public void testEmptyTenantList() throws Exception { + ListSamlProviderConfigsResponse response = + new ListSamlProviderConfigsResponse(ImmutableList.of(), "PAGE_TOKEN"); + + assertEquals(0, response.getProviderConfigs().size()); + assertFalse(response.hasProviderConfigs()); + } + + @Test + public void testDeserialization() throws Exception { + String json = JSON_FACTORY.toString( + ImmutableMap.of( + "inboundSamlConfigs", ImmutableList.of( + ImmutableMap.of("name", "projects/projectId/inboundSamlConfigs/saml.provider-id-1"), + ImmutableMap.of("name", "projects/projectId/inboundSamlConfigs/saml.provider-id-2")), + "nextPageToken", "PAGE_TOKEN")); + ListSamlProviderConfigsResponse response = + JSON_FACTORY.fromString(json, ListSamlProviderConfigsResponse.class); + + assertEquals(2, response.getProviderConfigs().size()); + assertEquals("saml.provider-id-1", response.getProviderConfigs().get(0).getProviderId()); + assertEquals("saml.provider-id-2", response.getProviderConfigs().get(1).getProviderId()); + assertTrue(response.hasProviderConfigs()); + assertEquals("PAGE_TOKEN", response.getPageToken()); + } +} diff --git a/src/test/java/com/google/firebase/auth/internal/ListTenantsResponseTest.java b/src/test/java/com/google/firebase/auth/internal/ListTenantsResponseTest.java new file mode 100644 index 000000000..7d65966bb --- /dev/null +++ b/src/test/java/com/google/firebase/auth/internal/ListTenantsResponseTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.multitenancy.Tenant; + +import org.junit.Test; + +public class ListTenantsResponseTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + @Test + public void testDefaultValues() throws Exception { + ListTenantsResponse response = new ListTenantsResponse(); + + assertEquals(0, response.getTenants().size()); + assertFalse(response.hasTenants()); + assertEquals("", response.getPageToken()); + } + + @Test + public void testEmptyTenantList() throws Exception { + ListTenantsResponse response = + new ListTenantsResponse(ImmutableList.of(), "PAGE_TOKEN"); + + assertEquals(0, response.getTenants().size()); + assertFalse(response.hasTenants()); + } + + @Test + public void testDeserialization() throws Exception { + String json = JSON_FACTORY.toString( + ImmutableMap.of( + "tenants", ImmutableList.of( + ImmutableMap.of("name", "projects/project-id/resource/TENANT_1"), + ImmutableMap.of("name", "projects/project-id/resource/TENANT_2")), + "pageToken", "PAGE_TOKEN")); + ListTenantsResponse response = JSON_FACTORY.fromString(json, ListTenantsResponse.class); + + assertEquals(2, response.getTenants().size()); + assertEquals("TENANT_1", response.getTenants().get(0).getTenantId()); + assertEquals("TENANT_2", response.getTenants().get(1).getTenantId()); + assertTrue(response.hasTenants()); + assertEquals("PAGE_TOKEN", response.getPageToken()); + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java new file mode 100644 index 000000000..36660dc28 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java @@ -0,0 +1,363 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +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.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.internal.SdkUtils; +import com.google.firebase.testing.MultiRequestMockHttpTransport; +import com.google.firebase.testing.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; +import org.junit.Test; + +public class FirebaseTenantClientTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + private static final String TEST_TOKEN = "token"; + + private static final GoogleCredentials credentials = new MockGoogleCredentials(TEST_TOKEN); + + private static final String PROJECT_BASE_URL = + "https://identitytoolkit.googleapis.com/v2/projects/test-project-id"; + + private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants"; + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testGetTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().getTenant("TENANT_1"); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_1"); + } + + @Test + public void testGetTenantWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getTenantManager().getTenant("UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/UNKNOWN"); + } + + @Test + public void testListTenants() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("listTenants.json")); + + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null, 99); + + ImmutableList tenants = ImmutableList.copyOf(page.getValues()); + assertEquals(2, tenants.size()); + checkTenant(tenants.get(0), "TENANT_1"); + checkTenant(tenants.get(1), "TENANT_2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("pageToken")); + } + + @Test + public void testListTenantsWithPageToken() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("listTenants.json")); + + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants("token", 99); + + ImmutableList tenants = ImmutableList.copyOf(page.getValues()); + assertEquals(2, tenants.size()); + checkTenant(tenants.get(0), "TENANT_1"); + checkTenant(tenants.get(1), "TENANT_2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertEquals("token", url.getFirst("pageToken")); + } + + @Test + public void testListZeroTenants() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantManagement("{}"); + + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null); + + assertTrue(Iterables.isEmpty(page.getValues())); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + } + + @Test + public void testCreateTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.CreateRequest request = new Tenant.CreateRequest() + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(true) + .setEmailLinkSignInEnabled(false); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertEquals(true, parsed.get("allowPasswordSignup")); + assertEquals(false, parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testCreateTenantMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.CreateRequest request = new Tenant.CreateRequest(); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL); + GenericJson parsed = parseRequestContent(interceptor); + assertNull(parsed.get("displayName")); + assertNull(parsed.get("allowPasswordSignup")); + assertNull(parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testCreateTenantError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + try { + FirebaseAuth.getInstance().getTenantManager().createTenant(new Tenant.CreateRequest()); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "POST", TENANTS_BASE_URL); + } + + @Test + public void testUpdateTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.UpdateRequest request = new Tenant.UpdateRequest("TENANT_1") + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(true) + .setEmailLinkSignInEnabled(false); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("allowPasswordSignup,displayName,enableEmailLinkSignin", + url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertEquals(true, parsed.get("allowPasswordSignup")); + assertEquals(false, parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testUpdateTenantMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.UpdateRequest request = + new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME"); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("displayName", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertNull(parsed.get("allowPasswordSignup")); + assertNull(parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testUpdateTenantNoValues() throws Exception { + initializeAppForTenantManagement(TestUtils.loadResource("tenant.json")); + TenantManager tenantManager = FirebaseAuth.getInstance().getTenantManager(); + try { + tenantManager.updateTenant(new Tenant.UpdateRequest("TENANT_1")); + fail("No error thrown for empty tenant update"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testUpdateTenantError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + Tenant.UpdateRequest request = + new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME"); + try { + FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); + } + + @Test + public void testDeleteTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement("{}"); + + FirebaseAuth.getInstance().getTenantManager().deleteTenant("TENANT_1"); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/TENANT_1"); + } + + @Test + public void testDeleteTenantWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getTenantManager().deleteTenant("UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/UNKNOWN"); + } + + private static void checkTenant(Tenant tenant, String tenantId) { + assertEquals(tenantId, tenant.getTenantId()); + assertEquals("DISPLAY_NAME", tenant.getDisplayName()); + assertTrue(tenant.isPasswordSignInAllowed()); + assertFalse(tenant.isEmailLinkSignInEnabled()); + } + + private static void checkRequestHeaders(TestResponseInterceptor interceptor) { + HttpHeaders headers = interceptor.getResponse().getRequest().getHeaders(); + String auth = "Bearer " + TEST_TOKEN; + assertEquals(auth, headers.getFirstHeaderStringValue("Authorization")); + + String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); + assertEquals(clientVersion, headers.getFirstHeaderStringValue("X-Client-Version")); + } + + private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) { + HttpRequest request = interceptor.getResponse().getRequest(); + if (method.equals("PATCH")) { + assertEquals("PATCH", + request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override")); + assertEquals("POST", request.getRequestMethod()); + } else { + assertEquals(method, request.getRequestMethod()); + } + assertEquals(url, request.getUrl().toString().split("\\?")[0]); + } + + private static TestResponseInterceptor initializeAppForTenantManagement(String... responses) { + initializeAppWithResponses(responses); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getTenantManager().setInterceptor(interceptor); + return interceptor; + } + + private static TestResponseInterceptor initializeAppForTenantManagementWithStatusCode( + int statusCode, String response) { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport( + new MockHttpTransport.Builder() + .setLowLevelHttpResponse( + new MockLowLevelHttpResponse().setContent(response).setStatusCode(statusCode)) + .build()) + .setProjectId("test-project-id") + .build()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getTenantManager().setInterceptor(interceptor); + return interceptor; + } + + private static void initializeAppWithResponses(String... responses) { + List mocks = new ArrayList<>(); + for (String response : responses) { + mocks.add(new MockLowLevelHttpResponse().setContent(response)); + } + MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks); + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport(transport) + .setProjectId("test-project-id") + .build()); + } + + private static GenericJson parseRequestContent(TestResponseInterceptor interceptor) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + return JSON_FACTORY.fromString(new String(out.toByteArray()), GenericJson.class); + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java b/src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java new file mode 100644 index 000000000..10830592f --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java @@ -0,0 +1,349 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.internal.ListTenantsResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.junit.Test; + +public class ListTenantsPageTest { + + @Test + public void testSinglePage() throws FirebaseAuthException, IOException { + TestTenantSource source = new TestTenantSource(3); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListTenantsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + + ImmutableList tenants = ImmutableList.copyOf(page.getValues()); + assertEquals(3, tenants.size()); + for (int i = 0; i < 3; i++) { + assertEquals("tenant" + i, tenants.get(i).getTenantId()); + } + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testMultiplePages() throws FirebaseAuthException, IOException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant0"), newTenant("tenant1"), newTenant("tenant2")), + "token"); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page1 = new ListTenantsPage.PageFactory(source).create(); + assertTrue(page1.hasNextPage()); + assertEquals("token", page1.getNextPageToken()); + ImmutableList tenants = ImmutableList.copyOf(page1.getValues()); + assertEquals(3, tenants.size()); + for (int i = 0; i < 3; i++) { + assertEquals("tenant" + i, tenants.get(i).getTenantId()); + } + + response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant3"), newTenant("tenant4"), newTenant("tenant5")), + ListTenantsPage.END_OF_LIST); + source.response = response; + ListTenantsPage page2 = page1.getNextPage(); + assertFalse(page2.hasNextPage()); + assertEquals(ListTenantsPage.END_OF_LIST, page2.getNextPageToken()); + tenants = ImmutableList.copyOf(page2.getValues()); + assertEquals(3, tenants.size()); + for (int i = 3; i < 6; i++) { + assertEquals("tenant" + i, tenants.get(i - 3).getTenantId()); + } + + assertEquals(2, source.calls.size()); + assertNull(source.calls.get(0)); + assertEquals("token", source.calls.get(1)); + + // Should iterate all tenants from both pages + int iterations = 0; + for (Tenant tenant : page1.iterateAll()) { + iterations++; + } + assertEquals(6, iterations); + assertEquals(3, source.calls.size()); + assertEquals("token", source.calls.get(2)); + + // Should only iterate tenants in the last page + iterations = 0; + for (Tenant tenant : page2.iterateAll()) { + iterations++; + } + assertEquals(3, iterations); + assertEquals(3, source.calls.size()); + } + + @Test + public void testListTenantsIterable() throws FirebaseAuthException, IOException { + TestTenantSource source = new TestTenantSource(3); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterable tenants = page.iterateAll(); + + int iterations = 0; + for (Tenant tenant : tenants) { + assertEquals("tenant" + iterations, tenant.getTenantId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + // Should result in a new iterator + iterations = 0; + for (Tenant tenant : tenants) { + assertEquals("tenant" + iterations, tenant.getTenantId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testListTenantsIterator() throws FirebaseAuthException, IOException { + TestTenantSource source = new TestTenantSource(3); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterable tenants = page.iterateAll(); + Iterator iterator = tenants.iterator(); + int iterations = 0; + while (iterator.hasNext()) { + assertEquals("tenant" + iterations, iterator.next().getTenantId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + while (iterator.hasNext()) { + fail("Should not be able to to iterate any more"); + } + try { + iterator.next(); + fail("Should not be able to iterate any more"); + } catch (NoSuchElementException expected) { + // expected + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testListTenantsPagedIterable() throws FirebaseAuthException, IOException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant0"), newTenant("tenant1"), newTenant("tenant2")), + "token"); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + int iterations = 0; + for (Tenant tenant : page.iterateAll()) { + assertEquals("tenant" + iterations, tenant.getTenantId()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant3"), newTenant("tenant4"), newTenant("tenant5")), + ListTenantsPage.END_OF_LIST); + source.response = response; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1)); + } + + @Test + public void testListTenantsPagedIterator() throws FirebaseAuthException, IOException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant0"), newTenant("tenant1"), newTenant("tenant2")), + "token"); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterator tenants = page.iterateAll().iterator(); + int iterations = 0; + while (tenants.hasNext()) { + assertEquals("tenant" + iterations, tenants.next().getTenantId()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant3"), newTenant("tenant4"), newTenant("tenant5")), + ListTenantsPage.END_OF_LIST); + source.response = response; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1)); + assertFalse(tenants.hasNext()); + try { + tenants.next(); + } catch (NoSuchElementException e) { + // expected + } + } + + @Test + public void testPageWithNoTenants() throws FirebaseAuthException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(), + ListTenantsPage.END_OF_LIST); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListTenantsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + assertEquals(0, ImmutableList.copyOf(page.getValues()).size()); + assertEquals(1, source.calls.size()); + } + + @Test + public void testIterableWithNoTenants() throws FirebaseAuthException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(), + ListTenantsPage.END_OF_LIST); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + for (Tenant tenant : page.iterateAll()) { + fail("Should not be able to iterate, but got: " + tenant); + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testIteratorWithNoTenants() throws FirebaseAuthException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(), + ListTenantsPage.END_OF_LIST); + TestTenantSource source = new TestTenantSource(response); + + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + while (iterator.hasNext()) { + fail("Should not be able to iterate"); + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testRemove() throws FirebaseAuthException, IOException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant1")), + ListTenantsPage.END_OF_LIST); + TestTenantSource source = new TestTenantSource(response); + + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + while (iterator.hasNext()) { + assertNotNull(iterator.next()); + try { + iterator.remove(); + } catch (UnsupportedOperationException expected) { + // expected + } + } + } + + @Test(expected = NullPointerException.class) + public void testNullSource() { + new ListTenantsPage.PageFactory(null); + } + + @Test + public void testInvalidPageToken() throws IOException { + TestTenantSource source = new TestTenantSource(1); + try { + new ListTenantsPage.PageFactory(source, 1000, ""); + fail("No error thrown for empty page token"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidMaxResults() throws IOException { + TestTenantSource source = new TestTenantSource(1); + try { + new ListTenantsPage.PageFactory(source, 1001, ""); + fail("No error thrown for maxResult > 1000"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ListTenantsPage.PageFactory(source, 0, "next"); + fail("No error thrown for maxResult = 0"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ListTenantsPage.PageFactory(source, -1, "next"); + fail("No error thrown for maxResult < 0"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + private static Tenant newTenant(String tenantId) throws IOException { + return Utils.getDefaultJsonFactory().fromString( + String.format("{\"name\":\"%s\"}", tenantId), Tenant.class); + } + + private static class TestTenantSource implements ListTenantsPage.TenantSource { + + private ListTenantsResponse response; + private final List calls = new ArrayList<>(); + + TestTenantSource(int tenantCount) throws IOException { + ImmutableList.Builder tenants = ImmutableList.builder(); + for (int i = 0; i < tenantCount; i++) { + tenants.add(newTenant("tenant" + i)); + } + this.response = new ListTenantsResponse(tenants.build(), ListTenantsPage.END_OF_LIST); + } + + TestTenantSource(ListTenantsResponse response) { + this.response = response; + } + + @Override + public ListTenantsResponse fetch(int maxResults, String pageToken) { + calls.add(pageToken); + return response; + } + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java new file mode 100644 index 000000000..61a841902 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java @@ -0,0 +1,450 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.FirebaseApp; +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.ListProviderConfigsPage; +import com.google.firebase.auth.ListUsersPage; +import com.google.firebase.auth.OidcProviderConfig; +import com.google.firebase.auth.ProviderConfigTestUtils; +import com.google.firebase.auth.ProviderConfigTestUtils.TemporaryProviderConfig; +import com.google.firebase.auth.SamlProviderConfig; +import com.google.firebase.auth.UserRecord; +import com.google.firebase.auth.UserTestUtils; +import com.google.firebase.auth.UserTestUtils.RandomUser; +import com.google.firebase.auth.UserTestUtils.TemporaryUser; +import com.google.firebase.internal.Nullable; +import com.google.firebase.testing.IntegrationTestUtils; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +public class TenantAwareFirebaseAuthIT { + + private static final String VERIFY_CUSTOM_TOKEN_URL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken"; + private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + private static final HttpTransport transport = Utils.getDefaultTransport(); + + private static TenantManager tenantManager; + private static TenantAwareFirebaseAuth tenantAwareAuth; + private static String tenantId; + + @Rule public final TemporaryUser temporaryUser = new TemporaryUser(tenantAwareAuth); + @Rule public final TemporaryProviderConfig temporaryProviderConfig = + new TemporaryProviderConfig(tenantAwareAuth); + + @BeforeClass + public static void setUpClass() throws Exception { + FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp(); + tenantManager = FirebaseAuth.getInstance(masterApp).getTenantManager(); + Tenant.CreateRequest tenantCreateRequest = + new Tenant.CreateRequest().setDisplayName("DisplayName"); + tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId(); + tenantAwareAuth = tenantManager.getAuthForTenant(tenantId); + } + + @AfterClass + public static void tearDownClass() throws Exception { + tenantManager.deleteTenant(tenantId); + } + + @Test + public void testUserLifecycle() throws Exception { + // Create user + UserRecord userRecord = temporaryUser.create(new UserRecord.CreateRequest()); + String uid = userRecord.getUid(); + + // Get user + userRecord = tenantAwareAuth.getUserAsync(userRecord.getUid()).get(); + assertEquals(uid, userRecord.getUid()); + assertEquals(tenantId, userRecord.getTenantId()); + assertNull(userRecord.getDisplayName()); + assertNull(userRecord.getEmail()); + assertNull(userRecord.getPhoneNumber()); + assertNull(userRecord.getPhotoUrl()); + assertFalse(userRecord.isEmailVerified()); + assertFalse(userRecord.isDisabled()); + assertTrue(userRecord.getUserMetadata().getCreationTimestamp() > 0); + assertEquals(0, userRecord.getUserMetadata().getLastSignInTimestamp()); + assertEquals(0, userRecord.getProviderData().length); + assertTrue(userRecord.getCustomClaims().isEmpty()); + + // Update user + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + UserRecord.UpdateRequest request = userRecord.updateRequest() + .setDisplayName("Updated Name") + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) + .setPhotoUrl("https://example.com/photo.png") + .setEmailVerified(true) + .setPassword("secret"); + userRecord = tenantAwareAuth.updateUserAsync(request).get(); + assertEquals(uid, userRecord.getUid()); + assertEquals(tenantId, userRecord.getTenantId()); + assertEquals("Updated Name", userRecord.getDisplayName()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); + assertEquals(randomUser.getPhoneNumber(), userRecord.getPhoneNumber()); + assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); + assertTrue(userRecord.isEmailVerified()); + assertFalse(userRecord.isDisabled()); + assertEquals(2, userRecord.getProviderData().length); + assertTrue(userRecord.getCustomClaims().isEmpty()); + + // Get user by email + userRecord = tenantAwareAuth.getUserByEmailAsync(userRecord.getEmail()).get(); + assertEquals(uid, userRecord.getUid()); + + // Disable user and remove properties + request = userRecord.updateRequest() + .setPhotoUrl(null) + .setDisplayName(null) + .setPhoneNumber(null) + .setDisabled(true); + userRecord = tenantAwareAuth.updateUserAsync(request).get(); + assertEquals(uid, userRecord.getUid()); + assertEquals(tenantId, userRecord.getTenantId()); + assertNull(userRecord.getDisplayName()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); + assertNull(userRecord.getPhoneNumber()); + assertNull(userRecord.getPhotoUrl()); + assertTrue(userRecord.isEmailVerified()); + assertTrue(userRecord.isDisabled()); + assertEquals(1, userRecord.getProviderData().length); + assertTrue(userRecord.getCustomClaims().isEmpty()); + + // Delete user + tenantAwareAuth.deleteUserAsync(userRecord.getUid()).get(); + UserTestUtils.assertUserDoesNotExist(tenantAwareAuth, userRecord.getUid()); + } + + @Test + public void testListUsers() throws Exception { + final List uids = new ArrayList<>(); + + for (int i = 0; i < 3; i++) { + UserRecord.CreateRequest createRequest = + new UserRecord.CreateRequest().setPassword("password"); + uids.add(temporaryUser.create(createRequest).getUid()); + } + + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListUsersPage page = tenantAwareAuth.listUsersAsync(null).get(); + while (page != null) { + for (ExportedUserRecord user : page.getValues()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull("Missing passwordHash field. A common cause would be " + + "forgetting to add the \"Firebase Authentication Admin\" permission. See " + + "instructions in CONTRIBUTING.md", user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertEquals(tenantId, user.getTenantId()); + } + } + page = page.getNextPage(); + } + assertEquals(uids.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = tenantAwareAuth.listUsersAsync(null).get(); + for (ExportedUserRecord user : page.iterateAll()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull(user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertEquals(tenantId, user.getTenantId()); + } + } + assertEquals(uids.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference error = new AtomicReference<>(); + ApiFuture pageFuture = tenantAwareAuth.listUsersAsync(null); + ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } + + @Override + public void onSuccess(ListUsersPage result) { + for (ExportedUserRecord user : result.iterateAll()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull(user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertEquals(tenantId, user.getTenantId()); + } + } + semaphore.release(); + } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(uids.size(), collected.get()); + assertNull(error.get()); + } + + @Test + public void testCustomToken() throws Exception { + String customToken = tenantAwareAuth.createCustomTokenAsync("user1").get(); + String idToken = signInWithCustomToken(customToken, tenantId); + FirebaseToken decoded = tenantAwareAuth.verifyIdTokenAsync(idToken).get(); + assertEquals("user1", decoded.getUid()); + assertEquals(tenantId, decoded.getTenantId()); + } + + @Test + public void testVerifyTokenWithWrongTenantAwareClient() throws Exception { + String customToken = tenantAwareAuth.createCustomTokenAsync("user").get(); + String idToken = signInWithCustomToken(customToken, tenantId); + + try { + tenantManager.getAuthForTenant("OTHER").verifyIdTokenAsync(idToken).get(); + fail("No error thrown for verifying a token with the wrong tenant-aware client"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals("tenant-id-mismatch", + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + @Test + public void testOidcProviderConfigLifecycle() throws Exception { + // Create provider config + String providerId = "oidc.provider-id"; + OidcProviderConfig config = + temporaryProviderConfig.createOidcProviderConfig( + new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setDisplayName("DisplayName") + .setEnabled(true) + .setClientId("ClientId") + .setIssuer("https://oidc.com/issuer")); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // Get provider config + config = tenantAwareAuth.getOidcProviderConfigAsync(providerId).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // Update provider config + OidcProviderConfig.UpdateRequest updateRequest = + new OidcProviderConfig.UpdateRequest(providerId) + .setDisplayName("NewDisplayName") + .setEnabled(false) + .setClientId("NewClientId") + .setIssuer("https://oidc.com/new-issuer"); + config = tenantAwareAuth.updateOidcProviderConfigAsync(updateRequest).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("NewDisplayName", config.getDisplayName()); + assertFalse(config.isEnabled()); + assertEquals("NewClientId", config.getClientId()); + assertEquals("https://oidc.com/new-issuer", config.getIssuer()); + + // Delete provider config + temporaryProviderConfig.deleteOidcProviderConfig(providerId); + ProviderConfigTestUtils.assertOidcProviderConfigDoesNotExist(tenantAwareAuth, providerId); + } + + @Test + public void testListOidcProviderConfigs() throws Exception { + final List providerIds = new ArrayList<>(); + + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "oidc.provider-id" + i; + providerIds.add(providerId); + temporaryProviderConfig.createOidcProviderConfig( + new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer")); + } + + // List provider configs + // NOTE: We do not need to test all of the different ways we can iterate over the provider + // configs, since this testing is already performed in FirebaseAuthIT with the tenant-agnostic + // tests. + final AtomicInteger collected = new AtomicInteger(0); + ListProviderConfigsPage page = + tenantAwareAuth.listOidcProviderConfigsAsync(null).get(); + for (OidcProviderConfig providerConfig : page.iterateAll()) { + if (providerIds.contains(providerConfig.getProviderId())) { + collected.incrementAndGet(); + assertEquals("CLIENT_ID", providerConfig.getClientId()); + assertEquals("https://oidc.com/issuer", providerConfig.getIssuer()); + } + } + assertEquals(providerIds.size(), collected.get()); + } + + @Test + public void testSamlProviderConfigLifecycle() throws Exception { + // Create provider config + String providerId = "saml.provider-id"; + SamlProviderConfig config = temporaryProviderConfig.createSamlProviderConfig( + new SamlProviderConfig.CreateRequest() + .setProviderId(providerId) + .setDisplayName("DisplayName") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler")); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + + config = tenantAwareAuth.getSamlProviderConfig(providerId); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + + // Update provider config + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest(providerId) + .setDisplayName("NewDisplayName") + .setEnabled(false) + .addX509Certificate("certificate"); + config = tenantAwareAuth.updateSamlProviderConfigAsync(updateRequest).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("NewDisplayName", config.getDisplayName()); + assertFalse(config.isEnabled()); + assertEquals(ImmutableList.of("certificate"), config.getX509Certificates()); + + // Delete provider config + temporaryProviderConfig.deleteSamlProviderConfig(providerId); + ProviderConfigTestUtils.assertSamlProviderConfigDoesNotExist(tenantAwareAuth, providerId); + } + + @Test + public void testListSamlProviderConfigs() throws Exception { + final List providerIds = new ArrayList<>(); + + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "saml.provider-id" + i; + providerIds.add(providerId); + temporaryProviderConfig.createSamlProviderConfig( + new SamlProviderConfig.CreateRequest() + .setProviderId(providerId) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler")); + } + + // List provider configs + // NOTE: We do not need to test all of the different ways we can iterate over the provider + // configs, since this testing is already performed in FirebaseAuthIT with the tenant-agnostic + // tests. + final AtomicInteger collected = new AtomicInteger(0); + ListProviderConfigsPage page = + tenantAwareAuth.listSamlProviderConfigsAsync(null).get(); + for (SamlProviderConfig config : page.iterateAll()) { + if (providerIds.contains(config.getProviderId())) { + collected.incrementAndGet(); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + } + } + assertEquals(providerIds.size(), collected.get()); + } + + private String signInWithCustomToken( + String customToken, @Nullable String tenantId) throws IOException { + final GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key=" + + IntegrationTestUtils.getApiKey()); + ImmutableMap.Builder content = ImmutableMap.builder(); + content.put("token", customToken); + content.put("returnSecureToken", true); + if (tenantId != null) { + content.put("tenantId", tenantId); + } + HttpRequest request = transport.createRequestFactory().buildPostRequest(url, + new JsonHttpContent(jsonFactory, content.build())); + request.setParser(new JsonObjectParser(jsonFactory)); + HttpResponse response = request.execute(); + try { + GenericJson json = response.parseAs(GenericJson.class); + return json.get("idToken").toString(); + } finally { + response.disconnect(); + } + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java new file mode 100644 index 000000000..086e25aa2 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java @@ -0,0 +1,158 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.testing.IntegrationTestUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Test; + +public class TenantManagerIT { + + private static final FirebaseAuth auth = FirebaseAuth.getInstance( + IntegrationTestUtils.ensureDefaultApp()); + + @Test + public void testTenantLifecycle() throws Exception { + TenantManager tenantManager = auth.getTenantManager(); + + // Create tenant + Tenant.CreateRequest createRequest = new Tenant.CreateRequest().setDisplayName("DisplayName"); + Tenant tenant = tenantManager.createTenantAsync(createRequest).get(); + assertEquals("DisplayName", tenant.getDisplayName()); + assertFalse(tenant.isPasswordSignInAllowed()); + assertFalse(tenant.isEmailLinkSignInEnabled()); + String tenantId = tenant.getTenantId(); + + // Get tenant + tenant = tenantManager.getTenantAsync(tenantId).get(); + assertEquals(tenantId, tenant.getTenantId()); + assertEquals("DisplayName", tenant.getDisplayName()); + assertFalse(tenant.isPasswordSignInAllowed()); + assertFalse(tenant.isEmailLinkSignInEnabled()); + + // Update tenant + Tenant.UpdateRequest updateRequest = tenant.updateRequest() + .setDisplayName("UpdatedName") + .setPasswordSignInAllowed(true) + .setEmailLinkSignInEnabled(true); + tenant = tenantManager.updateTenantAsync(updateRequest).get(); + assertEquals(tenantId, tenant.getTenantId()); + assertEquals("UpdatedName", tenant.getDisplayName()); + assertTrue(tenant.isPasswordSignInAllowed()); + assertTrue(tenant.isEmailLinkSignInEnabled()); + + // Delete tenant + tenantManager.deleteTenantAsync(tenant.getTenantId()).get(); + try { + tenantManager.getTenantAsync(tenant.getTenantId()).get(); + fail("No error thrown for getting a deleted tenant"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + @Test + public void testListTenants() throws Exception { + TenantManager tenantManager = auth.getTenantManager(); + final List tenantIds = new ArrayList<>(); + + try { + for (int i = 0; i < 3; i++) { + Tenant.CreateRequest createRequest = + new Tenant.CreateRequest().setDisplayName("DisplayName" + i); + tenantIds.add(tenantManager.createTenantAsync(createRequest).get().getTenantId()); + } + + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListTenantsPage page = tenantManager.listTenantsAsync(null).get(); + while (page != null) { + for (Tenant tenant : page.getValues()) { + if (tenantIds.contains(tenant.getTenantId())) { + collected.incrementAndGet(); + assertNotNull(tenant.getDisplayName()); + } + } + page = page.getNextPage(); + } + assertEquals(tenantIds.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = tenantManager.listTenantsAsync(null).get(); + for (Tenant tenant : page.iterateAll()) { + if (tenantIds.contains(tenant.getTenantId())) { + collected.incrementAndGet(); + assertNotNull(tenant.getDisplayName()); + } + } + assertEquals(tenantIds.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference error = new AtomicReference<>(); + ApiFuture pageFuture = tenantManager.listTenantsAsync(null); + ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } + + @Override + public void onSuccess(ListTenantsPage result) { + for (Tenant tenant : result.iterateAll()) { + if (tenantIds.contains(tenant.getTenantId())) { + collected.incrementAndGet(); + assertNotNull(tenant.getDisplayName()); + } + } + semaphore.release(); + } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(tenantIds.size(), collected.get()); + assertNull(error.get()); + } finally { + for (String tenantId : tenantIds) { + tenantManager.deleteTenantAsync(tenantId).get(); + } + } + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java new file mode 100644 index 000000000..7ea4f2539 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import java.io.IOException; +import java.util.Map; +import org.junit.Test; + +public class TenantTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + private static final String TENANT_JSON_STRING = + "{" + + "\"name\":\"projects/project-id/resource/TENANT_ID\"," + + "\"displayName\":\"DISPLAY_NAME\"," + + "\"allowPasswordSignup\":true," + + "\"enableEmailLinkSignin\":false" + + "}"; + + @Test + public void testJsonDeserialization() throws IOException { + Tenant tenant = JSON_FACTORY.fromString(TENANT_JSON_STRING, Tenant.class); + + assertEquals(tenant.getTenantId(), "TENANT_ID"); + assertEquals(tenant.getDisplayName(), "DISPLAY_NAME"); + assertTrue(tenant.isPasswordSignInAllowed()); + assertFalse(tenant.isEmailLinkSignInEnabled()); + } + + @Test + public void testUpdateRequestFromTenant() throws IOException { + Tenant tenant = JSON_FACTORY.fromString(TENANT_JSON_STRING, Tenant.class); + + Tenant.UpdateRequest updateRequest = tenant.updateRequest(); + + assertEquals("TENANT_ID", updateRequest.getTenantId()); + assertTrue(updateRequest.getProperties().isEmpty()); + } + + @Test + public void testUpdateRequestFromTenantId() throws IOException { + Tenant.UpdateRequest updateRequest = new Tenant.UpdateRequest("TENANT_ID"); + updateRequest + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(false) + .setEmailLinkSignInEnabled(true); + + assertEquals("TENANT_ID", updateRequest.getTenantId()); + Map properties = updateRequest.getProperties(); + assertEquals(properties.size(), 3); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("allowPasswordSignup")); + assertTrue((boolean) properties.get("enableEmailLinkSignin")); + } + + @Test + public void testCreateRequest() throws IOException { + Tenant.CreateRequest createRequest = new Tenant.CreateRequest(); + createRequest + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(false) + .setEmailLinkSignInEnabled(true); + + Map properties = createRequest.getProperties(); + assertEquals(properties.size(), 3); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("allowPasswordSignup")); + assertTrue((boolean) properties.get("enableEmailLinkSignin")); + } +} + diff --git a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java index d8395035e..ba7816173 100644 --- a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java +++ b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; +import com.google.auth.oauth2.GoogleCredentials; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -33,11 +34,12 @@ import com.google.firebase.database.util.EmulatorHelper; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; +import java.io.IOException; import java.util.List; import org.junit.Test; public class FirebaseDatabaseTest { - + private static final FirebaseOptions firebaseOptions = new FirebaseOptions.Builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) @@ -198,7 +200,7 @@ public void testInitAfterAppDelete() { } @Test - public void testDbUrlIsEmulatorUrlWhenSettingOptionsManually() { + public void testDbUrlIsEmulatorUrlWhenSettingOptionsManually() throws IOException { List testCases = ImmutableList.of( // cases where the env var is ignored because the supplied DB URL is a valid emulator URL @@ -235,7 +237,7 @@ public void testDbUrlIsEmulatorUrlWhenSettingOptionsManually() { } @Test - public void testDbUrlIsEmulatorUrlForDbRefWithPath() { + public void testDbUrlIsEmulatorUrlForDbRefWithPath() throws IOException { List testCases = ImmutableList.of( new CustomTestCase("http://my-custom-hosted-emulator.com:80?ns=dummy-ns", diff --git a/src/test/resources/getUser.json b/src/test/resources/getUser.json index 019c665be..3d57f1082 100644 --- a/src/test/resources/getUser.json +++ b/src/test/resources/getUser.json @@ -24,6 +24,7 @@ "validSince" : "1494364393", "disabled" : false, "createdAt" : "1234567890", - "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}" + "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}", + "tenantId": "testTenant" } ] } diff --git a/src/test/resources/listOidc.json b/src/test/resources/listOidc.json new file mode 100644 index 000000000..0c13ea48b --- /dev/null +++ b/src/test/resources/listOidc.json @@ -0,0 +1,15 @@ +{ + "oauthIdpConfigs" : [ { + "name": "projects/projectId/oauthIdpConfigs/oidc.provider-id1", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "clientId" : "CLIENT_ID", + "issuer" : "https://oidc.com/issuer" + }, { + "name": "projects/projectId/oauthIdpConfigs/oidc.provider-id2", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "clientId" : "CLIENT_ID", + "issuer" : "https://oidc.com/issuer" + } ] +} diff --git a/src/test/resources/listSaml.json b/src/test/resources/listSaml.json new file mode 100644 index 000000000..64b1e1e36 --- /dev/null +++ b/src/test/resources/listSaml.json @@ -0,0 +1,35 @@ +{ + "inboundSamlConfigs" : [ { + "name": "projects/projectId/inboundSamlConfigs/saml.provider-id1", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "idpConfig": { + "idpEntityId": "IDP_ENTITY_ID", + "ssoUrl": "https://example.com/login", + "idpCertificates": [ + { "x509Certificate": "certificate1" }, + { "x509Certificate": "certificate2" } + ] + }, + "spConfig": { + "spEntityId": "RP_ENTITY_ID", + "callbackUri": "https://projectId.firebaseapp.com/__/auth/handler" + } + }, { + "name": "projects/projectId/inboundSamlConfigs/saml.provider-id2", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "idpConfig": { + "idpEntityId": "IDP_ENTITY_ID", + "ssoUrl": "https://example.com/login", + "idpCertificates": [ + { "x509Certificate": "certificate1" }, + { "x509Certificate": "certificate2" } + ] + }, + "spConfig": { + "spEntityId": "RP_ENTITY_ID", + "callbackUri": "https://projectId.firebaseapp.com/__/auth/handler" + } + } ] +} diff --git a/src/test/resources/listTenants.json b/src/test/resources/listTenants.json new file mode 100644 index 000000000..1e2f3a01a --- /dev/null +++ b/src/test/resources/listTenants.json @@ -0,0 +1,17 @@ +{ + "tenants" : [ { + "name" : "TENANT_1", + "displayName" : "DISPLAY_NAME", + "allowPasswordSignup" : true, + "enableEmailLinkSignin" : false, + "disableAuth" : true, + "enableAnonymousUser" : false + }, { + "name" : "TENANT_2", + "displayName" : "DISPLAY_NAME", + "allowPasswordSignup" : true, + "enableEmailLinkSignin" : false, + "disableAuth" : true, + "enableAnonymousUser" : false + } ] +} diff --git a/src/test/resources/listUsers.json b/src/test/resources/listUsers.json index 06d0b34c9..47e169709 100644 --- a/src/test/resources/listUsers.json +++ b/src/test/resources/listUsers.json @@ -24,7 +24,8 @@ "validSince" : "1494364393", "disabled" : false, "createdAt" : "1234567890", - "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}" + "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}", + "tenantId": "testTenant" }, { "localId" : "testuser", "email" : "testuser@example.com", @@ -50,6 +51,7 @@ "validSince" : "1494364393", "disabled" : false, "createdAt" : "1234567890", - "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}" + "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}", + "tenantId": "testTenant" } ] } diff --git a/src/test/resources/oidc.json b/src/test/resources/oidc.json new file mode 100644 index 000000000..e2f1845de --- /dev/null +++ b/src/test/resources/oidc.json @@ -0,0 +1,7 @@ +{ + "name": "projects/projectId/oauthIdpConfigs/oidc.provider-id", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "clientId" : "CLIENT_ID", + "issuer" : "https://oidc.com/issuer" +} diff --git a/src/test/resources/saml.json b/src/test/resources/saml.json new file mode 100644 index 000000000..ef425b0a8 --- /dev/null +++ b/src/test/resources/saml.json @@ -0,0 +1,17 @@ +{ + "name": "projects/projectId/inboundSamlConfigs/saml.provider-id", + "displayName": "DISPLAY_NAME", + "enabled": true, + "idpConfig": { + "idpEntityId": "IDP_ENTITY_ID", + "ssoUrl": "https://example.com/login", + "idpCertificates": [ + { "x509Certificate": "certificate1" }, + { "x509Certificate": "certificate2" } + ] + }, + "spConfig": { + "spEntityId": "RP_ENTITY_ID", + "callbackUri": "https://projectId.firebaseapp.com/__/auth/handler" + } +} diff --git a/src/test/resources/tenant.json b/src/test/resources/tenant.json new file mode 100644 index 000000000..cc8565f8b --- /dev/null +++ b/src/test/resources/tenant.json @@ -0,0 +1,8 @@ +{ + "name" : "TENANT_1", + "displayName" : "DISPLAY_NAME", + "allowPasswordSignup" : true, + "enableEmailLinkSignin" : false, + "disableAuth" : true, + "enableAnonymousUser" : false +} From d347ad0c1737cc55f1c3d2b38a53646c43bd83bd Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 23 Jul 2020 14:41:55 -0400 Subject: [PATCH 124/456] [chore] Release 6.15.0 (#457) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e9a2fc038..adbf23931 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.14.0 + 6.15.0 jar firebase-admin From 7b991238067137818907847826513bbd62a8f68a Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 28 Jul 2020 12:59:01 -0700 Subject: [PATCH 125/456] chore(auth): Added snippets for multitenancy and IdP management (#461) * chore(auth): Added snippets for multitenancy and IdP management * fix: Fixed some broken snippet tags * fix(auth): Fixed a typo and clarified a comment --- .../snippets/FirebaseAuthSnippets.java | 478 +++++++++++++++++- 1 file changed, 474 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java index fec3f5d2d..b6103b4ef 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java @@ -24,8 +24,12 @@ import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.FirebaseToken; import com.google.firebase.auth.ImportUserRecord; +import com.google.firebase.auth.ListProviderConfigsPage; import com.google.firebase.auth.ListUsersPage; +import com.google.firebase.auth.OidcProviderConfig; +import com.google.firebase.auth.SamlProviderConfig; import com.google.firebase.auth.SessionCookieOptions; +import com.google.firebase.auth.UserImportHash; import com.google.firebase.auth.UserImportOptions; import com.google.firebase.auth.UserImportResult; import com.google.firebase.auth.UserProvider; @@ -37,6 +41,10 @@ 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.auth.multitenancy.ListTenantsPage; +import com.google.firebase.auth.multitenancy.Tenant; +import com.google.firebase.auth.multitenancy.TenantAwareFirebaseAuth; +import com.google.firebase.auth.multitenancy.TenantManager; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.FirebaseDatabase; import java.net.URI; @@ -623,7 +631,7 @@ public void generatePasswordResetLink() { email, actionCodeSettings); // Construct email verification template, embed the link and send // using custom SMTP server. - sendCustomPasswordResetEmail(email, displayName, link); + sendCustomEmail(email, displayName, link); } catch (FirebaseAuthException e) { System.out.println("Error generating email link: " + e.getMessage()); } @@ -640,7 +648,7 @@ public void generateEmailVerificationLink() { email, actionCodeSettings); // Construct email verification template, embed the link and send // using custom SMTP server. - sendCustomPasswordResetEmail(email, displayName, link); + sendCustomEmail(email, displayName, link); } catch (FirebaseAuthException e) { System.out.println("Error generating email link: " + e.getMessage()); } @@ -657,14 +665,476 @@ public void generateSignInWithEmailLink() { email, actionCodeSettings); // Construct email verification template, embed the link and send // using custom SMTP server. - sendCustomPasswordResetEmail(email, displayName, link); + sendCustomEmail(email, displayName, link); } catch (FirebaseAuthException e) { System.out.println("Error generating email link: " + e.getMessage()); } // [END sign_in_with_email_link] } + // ===================================================================================== + // https://cloud.google.com/identity-platform/docs/managing-providers-programmatically + // ===================================================================================== + + public void createSamlProviderConfig() throws FirebaseAuthException { + // [START create_saml_provider] + SamlProviderConfig.CreateRequest request = new SamlProviderConfig.CreateRequest() + .setDisplayName("SAML provider name") + .setEnabled(true) + .setProviderId("saml.myProvider") + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/saml/sso/1234/") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT1...\n-----END CERTIFICATE-----") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://project-id.firebaseapp.com/__/auth/handler"); + SamlProviderConfig saml = FirebaseAuth.getInstance().createSamlProviderConfig(request); + System.out.println("Created new SAML provider: " + saml.getProviderId()); + // [END create_saml_provider] + } + + public void updateSamlProviderConfig() throws FirebaseAuthException { + // [START update_saml_provider] + SamlProviderConfig.UpdateRequest request = + new SamlProviderConfig.UpdateRequest("saml.myProvider") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT3...\n-----END CERTIFICATE-----"); + SamlProviderConfig saml = FirebaseAuth.getInstance().updateSamlProviderConfig(request); + System.out.println("Updated SAML provider: " + saml.getProviderId()); + // [END update_saml_provider] + } + + public void getSamlProviderConfig() throws FirebaseAuthException { + // [START get_saml_provider] + SamlProviderConfig saml = FirebaseAuth.getInstance().getSamlProviderConfig("saml.myProvider"); + System.out.println(saml.getDisplayName() + ": " + saml.isEnabled()); + // [END get_saml_provider] + } + + public void deleteSamlProviderConfig() throws FirebaseAuthException { + // [START delete_saml_provider] + FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.myProvider"); + // [END delete_saml_provider] + } + + public void listSamlProviderConfigs() throws FirebaseAuthException { + // [START list_saml_providers] + ListProviderConfigsPage page = FirebaseAuth.getInstance() + .listSamlProviderConfigs("nextPageToken"); + for (SamlProviderConfig config : page.iterateAll()) { + System.out.println(config.getProviderId()); + } + // [END list_saml_providers] + } + + public void createOidcProviderConfig() throws FirebaseAuthException { + // [START create_oidc_provider] + OidcProviderConfig.CreateRequest request = new OidcProviderConfig.CreateRequest() + .setDisplayName("OIDC provider name") + .setEnabled(true) + .setProviderId("oidc.myProvider") + .setClientId("CLIENT_ID2") + .setIssuer("https://oidc.com/CLIENT_ID2"); + OidcProviderConfig oidc = FirebaseAuth.getInstance().createOidcProviderConfig(request); + System.out.println("Created new OIDC provider: " + oidc.getProviderId()); + // [END create_oidc_provider] + } + + public void updateOidcProviderConfig() throws FirebaseAuthException { + // [START update_oidc_provider] + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.myProvider") + .setDisplayName("OIDC provider name") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com"); + OidcProviderConfig oidc = FirebaseAuth.getInstance().updateOidcProviderConfig(request); + System.out.println("Updated OIDC provider: " + oidc.getProviderId()); + // [END update_oidc_provider] + } + + public void getOidcProviderConfig() throws FirebaseAuthException { + // [START get_oidc_provider] + OidcProviderConfig oidc = FirebaseAuth.getInstance().getOidcProviderConfig("oidc.myProvider"); + System.out.println(oidc.getDisplayName() + ": " + oidc.isEnabled()); + // [END get_oidc_provider] + } + + public void deleteOidcProviderConfig() throws FirebaseAuthException { + // [START delete_oidc_provider] + FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.myProvider"); + // [END delete_oidc_provider] + } + + public void listOidcProviderConfigs() throws FirebaseAuthException { + // [START list_oidc_providers] + ListProviderConfigsPage page = FirebaseAuth.getInstance() + .listOidcProviderConfigs("nextPageToken"); + for (OidcProviderConfig oidc : page.iterateAll()) { + System.out.println(oidc.getProviderId()); + } + // [END list_oidc_providers] + } + + // ================================================================================ + // https://cloud.google.com/identity-platform/docs/multi-tenancy-managing-tenants + // ================================================================================= + + public TenantAwareFirebaseAuth getTenantAwareFirebaseAuth(String tenantId) { + // [START get_tenant_client] + FirebaseAuth auth = FirebaseAuth.getInstance(); + TenantManager tenantManager = auth.getTenantManager(); + TenantAwareFirebaseAuth tenantAuth = tenantManager.getAuthForTenant(tenantId); + // [END get_tenant_client] + + return tenantAuth; + } + + public void getTenant(String tenantId) throws FirebaseAuthException { + // [START get_tenant] + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().getTenant(tenantId); + System.out.println("Retrieved tenant: " + tenant.getTenantId()); + // [END get_tenant] + } + + public void createTenant() throws FirebaseAuthException { + // [START create_tenant] + Tenant.CreateRequest request = new Tenant.CreateRequest() + .setDisplayName("myTenant1") + .setEmailLinkSignInEnabled(true) + .setPasswordSignInAllowed(true); + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request); + System.out.println("Created tenant: " + tenant.getTenantId()); + // [END create_tenant] + } + + public void updateTenant(String tenantId) throws FirebaseAuthException { + // [START update_tenant] + Tenant.UpdateRequest request = new Tenant.UpdateRequest(tenantId) + .setDisplayName("updatedName") + .setPasswordSignInAllowed(false); + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + System.out.println("Updated tenant: " + tenant.getTenantId()); + // [END update_tenant] + } + + public void deleteTenant(String tenantId) throws FirebaseAuthException { + // [START delete_tenant] + FirebaseAuth.getInstance().getTenantManager().deleteTenant(tenantId); + // [END delete_tenant] + } + + public void listTenants() throws FirebaseAuthException { + // [START list_tenants] + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null); + for (Tenant tenant : page.iterateAll()) { + System.out.println("Retrieved tenant: " + tenant.getTenantId()); + } + // [END list_tenants] + } + + public void createProviderTenant() throws FirebaseAuthException { + // [START get_tenant_client_short] + TenantAwareFirebaseAuth tenantAuth = FirebaseAuth.getInstance().getTenantManager() + .getAuthForTenant("TENANT-ID"); + // [END get_tenant_client_short] + + // [START create_saml_provider_tenant] + SamlProviderConfig.CreateRequest request = new SamlProviderConfig.CreateRequest() + .setDisplayName("SAML provider name") + .setEnabled(true) + .setProviderId("saml.myProvider") + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/saml/sso/1234/") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT1...\n-----END CERTIFICATE-----") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://project-id.firebaseapp.com/__/auth/handler"); + SamlProviderConfig saml = tenantAuth.createSamlProviderConfig(request); + System.out.println("Created new SAML provider: " + saml.getProviderId()); + // [END create_saml_provider_tenant] + } + + public void updateProviderTenant( + TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START update_saml_provider_tenant] + SamlProviderConfig.UpdateRequest request = + new SamlProviderConfig.UpdateRequest("saml.myProvider") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT3...\n-----END CERTIFICATE-----"); + SamlProviderConfig saml = tenantAuth.updateSamlProviderConfig(request); + System.out.println("Updated SAML provider: " + saml.getProviderId()); + // [END update_saml_provider_tenant] + } + + public void getProviderTenant(TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START get_saml_provider_tenant] + SamlProviderConfig saml = tenantAuth.getSamlProviderConfig("saml.myProvider"); + + // Get display name and whether it is enabled. + System.out.println(saml.getDisplayName() + " " + saml.isEnabled()); + // [END get_saml_provider_tenant] + } + + public void listProvidersTenant(TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START list_saml_providers_tenant] + ListProviderConfigsPage page = tenantAuth.listSamlProviderConfigs( + "nextPageToken"); + for (SamlProviderConfig saml : page.iterateAll()) { + System.out.println(saml.getProviderId()); + } + // [END list_saml_providers_tenant] + } + + public void deleteProviderTenant( + TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START delete_saml_provider_tenant] + tenantAuth.deleteSamlProviderConfig("saml.myProvider"); + // [END delete_saml_provider_tenant] + } + + public void getUserTenant( + TenantAwareFirebaseAuth tenantAuth, String uid) throws FirebaseAuthException { + // [START get_user_tenant] + // Get an auth client from the firebase.App + UserRecord user = tenantAuth.getUser(uid); + System.out.println("Successfully fetched user data: " + user.getDisplayName()); + // [END get_user_tenant] + } + + public void getUserByEmailTenant( + TenantAwareFirebaseAuth tenantAuth, String email) throws FirebaseAuthException { + // [START get_user_by_email_tenant] + // Get an auth client from the firebase.App + UserRecord user = tenantAuth.getUserByEmail(email); + System.out.println("Successfully fetched user data: " + user.getDisplayName()); + // [END get_user_by_email_tenant] + } + + public void createUserTenant(TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START create_user_tenant] + UserRecord.CreateRequest request = new UserRecord.CreateRequest() + .setEmail("user@example.com") + .setEmailVerified(false) + .setPhoneNumber("+15555550100") + .setPassword("secretPassword") + .setDisplayName("John Doe") + .setPhotoUrl("http://www.example.com/12345678/photo.png") + .setDisabled(false); + UserRecord user = tenantAuth.createUser(request); + System.out.println("Successfully created user: " + user.getDisplayName()); + // [END create_user_tenant] + } + + public void updateUserTenant( + TenantAwareFirebaseAuth tenantAuth, String uid) throws FirebaseAuthException { + // [START update_user_tenant] + UserRecord.UpdateRequest request = new UserRecord.UpdateRequest(uid) + .setEmail("user@example.com") + .setEmailVerified(true) + .setPhoneNumber("+15555550100") + .setPassword("newPassword") + .setDisplayName("John Doe") + .setPhotoUrl("http://www.example.com/12345678/photo.png") + .setDisabled(true); + UserRecord user = tenantAuth.updateUser(request); + System.out.println("Successfully updated user: " + user.getDisplayName()); + // [END update_user_tenant] + } + + public void deleteUserTenant( + TenantAwareFirebaseAuth tenantAuth, String uid) throws FirebaseAuthException { + // [START delete_user_tenant] + tenantAuth.deleteUser(uid); + + System.out.println("Successfully deleted user: " + uid); + // [END delete_user_tenant] + } + + public void listUsersTenant(TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START list_all_users_tenant] + // Note, behind the scenes, the ListUsersPage retrieves 1000 Users at a time + // through the API + ListUsersPage page = tenantAuth.listUsers(null); + for (ExportedUserRecord user : page.iterateAll()) { + System.out.println("User: " + user.getUid()); + } + + // Iterating by pages 100 users at a time. + page = tenantAuth.listUsers(null, 100); + while (page != null) { + for (ExportedUserRecord user : page.getValues()) { + System.out.println("User: " + user.getUid()); + } + + page = page.getNextPage(); + } + // [END list_all_users_tenant] + } + + public void importWithHmacTenant( + TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START import_with_hmac_tenant] + List users = new ArrayList<>(); + users.add(ImportUserRecord.builder() + .setUid("uid1") + .setEmail("user1@example.com") + .setPasswordHash("password-hash-1".getBytes()) + .setPasswordSalt("salt1".getBytes()) + .build()); + users.add(ImportUserRecord.builder() + .setUid("uid2") + .setEmail("user2@example.com") + .setPasswordHash("password-hash-2".getBytes()) + .setPasswordSalt("salt2".getBytes()) + .build()); + UserImportHash hmacSha256 = HmacSha256.builder() + .setKey("secret".getBytes()) + .build(); + UserImportResult result = tenantAuth.importUsers(users, UserImportOptions.withHash(hmacSha256)); + + for (ErrorInfo error : result.getErrors()) { + System.out.println("Failed to import user: " + error.getReason()); + } + // [END import_with_hmac_tenant] + } + + public void importWithoutPasswordTenant( + TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START import_without_password_tenant] + List users = new ArrayList<>(); + users.add(ImportUserRecord.builder() + .setUid("some-uid") + .setDisplayName("John Doe") + .setEmail("johndoe@acme.com") + .setPhotoUrl("https://www.example.com/12345678/photo.png") + .setEmailVerified(true) + .setPhoneNumber("+11234567890") + // Set this user as admin. + .putCustomClaim("admin", true) + // User with SAML provider. + .addUserProvider(UserProvider.builder() + .setUid("saml-uid") + .setEmail("johndoe@acme.com") + .setDisplayName("John Doe") + .setPhotoUrl("https://www.example.com/12345678/photo.png") + .setProviderId("saml.acme") + .build()) + .build()); + + UserImportResult result = tenantAuth.importUsers(users); + + for (ErrorInfo error : result.getErrors()) { + System.out.println("Failed to import user: " + error.getReason()); + } + // [END import_without_password_tenant] + } + + public void verifyIdTokenTenant(TenantAwareFirebaseAuth tenantAuth, String idToken) { + // [START verify_id_token_tenant] + try { + // idToken comes from the client app + FirebaseToken token = tenantAuth.verifyIdToken(idToken); + // TenantId on the FirebaseToken should be set to TENANT-ID. + // Otherwise "tenant-id-mismatch" error thrown. + System.out.println("Verified ID token from tenant: " + token.getTenantId()); + } catch (FirebaseAuthException e) { + System.out.println("error verifying ID token: " + e.getMessage()); + } + // [END verify_id_token_tenant] + } + + public void verifyIdTokenAccessControlTenant(TenantAwareFirebaseAuth tenantAuth, String idToken) { + // [START id_token_access_control_tenant] + try { + // idToken comes from the client app + FirebaseToken token = tenantAuth.verifyIdToken(idToken); + if ("TENANT-ID1".equals(token.getTenantId())) { + // Allow appropriate level of access for TENANT-ID1. + } else if ("TENANT-ID2".equals(token.getTenantId())) { + // Allow appropriate level of access for TENANT-ID2. + } else { + // Access not allowed -- Handle error + } + } catch (FirebaseAuthException e) { + System.out.println("error verifying ID token: " + e.getMessage()); + } + // [END id_token_access_control_tenant] + } + + public void revokeRefreshTokensTenant( + TenantAwareFirebaseAuth tenantAuth, String uid) throws FirebaseAuthException { + // [START revoke_tokens_tenant] + // Revoke all refresh tokens for a specified user in a specified tenant for whatever reason. + // Retrieve the timestamp of the revocation, in seconds since the epoch. + tenantAuth.revokeRefreshTokens(uid); + + // accessing the user's TokenValidAfter + UserRecord user = tenantAuth.getUser(uid); + + + long timestamp = user.getTokensValidAfterTimestamp() / 1000; + System.out.println("the refresh tokens were revoked at: " + timestamp + " (UTC seconds)"); + // [END revoke_tokens_tenant] + } + + public void verifyIdTokenAndCheckRevokedTenant( + TenantAwareFirebaseAuth tenantAuth, String idToken) { + // [START verify_id_token_and_check_revoked_tenant] + // Verify the ID token for a specific tenant while checking if the token is revoked. + boolean checkRevoked = true; + try { + FirebaseToken token = tenantAuth.verifyIdToken(idToken, checkRevoked); + System.out.println("Verified ID token for: " + token.getUid()); + } catch (FirebaseAuthException e) { + if ("id-token-revoked".equals(e.getErrorCode())) { + // Token is revoked. Inform the user to re-authenticate or signOut() the user. + } else { + // Token is invalid + } + } + // [END verify_id_token_and_check_revoked_tenant] + } + + public void customClaimsVerifyTenant( + TenantAwareFirebaseAuth tenantAuth, String idToken) throws FirebaseAuthException { + // [START verify_custom_claims_tenant] + // Verify the ID token first. + FirebaseToken token = tenantAuth.verifyIdToken(idToken); + if (Boolean.TRUE.equals(token.getClaims().get("admin"))) { + //Allow access to requested admin resource. + } + // [END verify_custom_claims_tenant] + } + + public void generateEmailVerificationLinkTenant( + TenantAwareFirebaseAuth tenantAuth, + String email, + String displayName) throws FirebaseAuthException { + // [START email_verification_link_tenant] + ActionCodeSettings actionCodeSettings = ActionCodeSettings.builder() + // URL you want to redirect back to. The domain (www.example.com) for + // this URL must be whitelisted in the GCP Console. + .setUrl("https://www.example.com/checkout?cartId=1234") + // This must be true for email link sign-in. + .setHandleCodeInApp(true) + .setIosBundleId("com.example.ios") + .setAndroidPackageName("com.example.android") + .setAndroidInstallApp(true) + .setAndroidMinimumVersion("12") + // FDL custom domain. + .setDynamicLinkDomain("coolapp.page.link") + .build(); + + String link = tenantAuth.generateEmailVerificationLink(email, actionCodeSettings); + + // Construct email verification template, embed the link and send + // using custom SMTP server. + sendCustomEmail(email, displayName, link); + // [END email_verification_link_tenant] + } + // 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) {} + private void sendCustomEmail(String email, String displayName, String link) {} } From 3564eeb7a6593fb7f23264c81c2f41d015c0ef8b Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 10 Aug 2020 10:07:34 -0700 Subject: [PATCH 126/456] feat(auth): Support for creating tenant-scoped session cookies (#467) * feat(auth): Support for creating tenant-scoped session cookies * fix: Cleaned up the unit test --- pom.xml | 4 +- .../firebase/auth/AbstractFirebaseAuth.java | 251 +++++++++++---- .../google/firebase/auth/FirebaseAuth.java | 209 +++---------- .../firebase/auth/FirebaseTokenUtils.java | 12 +- .../multitenancy/TenantAwareFirebaseAuth.java | 65 +++- .../auth/multitenancy/TenantManager.java | 2 +- .../firebase/auth/FirebaseAuthTest.java | 110 +++---- .../auth/FirebaseTokenVerifierImplTest.java | 13 + .../auth/FirebaseUserManagerTest.java | 25 +- .../firebase/auth/MockTokenVerifier.java | 59 ++++ .../TenantAwareFirebaseAuthTest.java | 292 ++++++++++++++++++ 11 files changed, 705 insertions(+), 337 deletions(-) create mode 100644 src/test/java/com/google/firebase/auth/MockTokenVerifier.java create mode 100644 src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java diff --git a/pom.xml b/pom.xml index adbf23931..b0187543a 100644 --- a/pom.xml +++ b/pom.xml @@ -198,7 +198,7 @@ org.jacoco jacoco-maven-plugin - 0.7.9 + 0.8.5 pre-unit-test @@ -289,7 +289,7 @@ maven-surefire-plugin - 2.19.1 + 2.22.0 ${skipUTs} diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index ad30d2cc3..6c841070c 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -62,50 +62,13 @@ public abstract class AbstractFirebaseAuth { private final Supplier userManager; private final JsonFactory jsonFactory; - protected AbstractFirebaseAuth(Builder builder) { + protected AbstractFirebaseAuth(Builder builder) { this.firebaseApp = checkNotNull(builder.firebaseApp); this.tokenFactory = threadSafeMemoize(builder.tokenFactory); this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier); this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier); this.userManager = threadSafeMemoize(builder.userManager); - this.jsonFactory = builder.firebaseApp.getOptions().getJsonFactory(); - } - - protected static Builder builderFromAppAndTenantId(final FirebaseApp app, final String tenantId) { - return AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setTokenFactory( - new Supplier() { - @Override - public FirebaseTokenFactory get() { - return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId); - } - }) - .setIdTokenVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId); - } - }) - .setCookieVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); - } - }) - .setUserManager( - new Supplier() { - @Override - public FirebaseUserManager get() { - return FirebaseUserManager - .builder() - .setFirebaseApp(app) - .setTenantId(tenantId) - .build(); - } - }); + this.jsonFactory = firebaseApp.getOptions().getJsonFactory(); } /** @@ -220,6 +183,51 @@ public String execute() throws FirebaseAuthException { }; } + /** + * Creates a new Firebase session cookie from the given ID token and options. The returned JWT can + * be set as a server-side session cookie with a custom cookie policy. + * + * @param idToken The Firebase ID token to exchange for a session cookie. + * @param options Additional options required to create the cookie. + * @return A Firebase session cookie string. + * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. + * @throws FirebaseAuthException If an error occurs while generating the session cookie. + */ + public String createSessionCookie(@NonNull String idToken, @NonNull SessionCookieOptions options) + throws FirebaseAuthException { + return createSessionCookieOp(idToken, options).call(); + } + + /** + * Similar to {@link #createSessionCookie(String, SessionCookieOptions)} but performs the + * operation asynchronously. + * + * @param idToken The Firebase ID token to exchange for a session cookie. + * @param options Additional options required to create the cookie. + * @return An {@code ApiFuture} which will complete successfully with a session cookie string. If + * an error occurs while generating the cookie or if the specified ID token is invalid, the + * future throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. + */ + public ApiFuture createSessionCookieAsync( + @NonNull String idToken, @NonNull SessionCookieOptions options) { + return createSessionCookieOp(idToken, options).callAsync(firebaseApp); + } + + private CallableOperation createSessionCookieOp( + final String idToken, final SessionCookieOptions options) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty"); + checkNotNull(options, "options must not be null"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected String execute() throws FirebaseAuthException { + return userManager.createSessionCookie(idToken, options); + } + }; + } + /** * Parses and verifies a Firebase ID Token. * @@ -320,6 +328,87 @@ FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) { return verifier; } + /** + * Parses and verifies a Firebase session cookie. + * + *

      If verified successfully, returns a parsed version of the cookie from which the UID and the + * other claims can be read. If the cookie is invalid, throws a {@link FirebaseAuthException}. + * + *

      This method does not check whether the cookie has been revoked. See {@link + * #verifySessionCookie(String, boolean)}. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @return A {@link FirebaseToken} representing the verified and decoded cookie. + */ + public FirebaseToken verifySessionCookie(String cookie) throws FirebaseAuthException { + return verifySessionCookie(cookie, false); + } + + /** + * Parses and verifies a Firebase session cookie. + * + *

      If {@code checkRevoked} is true, additionally verifies that the cookie has not been revoked. + * + *

      If verified successfully, returns a parsed version of the cookie from which the UID and the + * other claims can be read. If the cookie is invalid or has been revoked while {@code + * checkRevoked} is true, throws a {@link FirebaseAuthException}. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. + * @return A {@link FirebaseToken} representing the verified and decoded cookie. + */ + public FirebaseToken verifySessionCookie(String cookie, boolean checkRevoked) + throws FirebaseAuthException { + return verifySessionCookieOp(cookie, checkRevoked).call(); + } + + /** + * Similar to {@link #verifySessionCookie(String)} but performs the operation asynchronously. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or + * unsuccessfully with the failure Exception. + */ + public ApiFuture verifySessionCookieAsync(String cookie) { + return verifySessionCookieAsync(cookie, false); + } + + /** + * Similar to {@link #verifySessionCookie(String, boolean)} but performs the operation + * asynchronously. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. + * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or + * unsuccessfully with the failure Exception. + */ + public ApiFuture verifySessionCookieAsync(String cookie, boolean checkRevoked) { + return verifySessionCookieOp(cookie, checkRevoked).callAsync(firebaseApp); + } + + private CallableOperation verifySessionCookieOp( + final String cookie, final boolean checkRevoked) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty"); + final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked); + return new CallableOperation() { + @Override + public FirebaseToken execute() throws FirebaseAuthException { + return sessionCookieVerifier.verifyToken(cookie); + } + }; + } + + @VisibleForTesting + FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) { + FirebaseTokenVerifier verifier = cookieVerifier.get(); + if (checkRevoked) { + FirebaseUserManager userManager = getUserManager(); + verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager); + } + return verifier; + } + /** * Revokes all refresh tokens for the specified user. * @@ -1637,19 +1726,11 @@ protected Void execute() throws FirebaseAuthException { }; } - FirebaseApp getFirebaseApp() { - return this.firebaseApp; - } - - FirebaseTokenVerifier getCookieVerifier() { - return this.cookieVerifier.get(); - } - FirebaseUserManager getUserManager() { return this.userManager.get(); } - protected Supplier threadSafeMemoize(final Supplier supplier) { + Supplier threadSafeMemoize(final Supplier supplier) { return Suppliers.memoize( new Supplier() { @Override @@ -1663,7 +1744,7 @@ public T get() { }); } - void checkNotDestroyed() { + private void checkNotDestroyed() { synchronized (lock) { checkState( !destroyed.get(), @@ -1682,42 +1763,80 @@ final void destroy() { /** Performs any additional required clean up. */ protected abstract void doDestroy(); - static Builder builder() { - return new Builder(); - } + protected abstract static class Builder> { - static class Builder { - protected FirebaseApp firebaseApp; + private FirebaseApp firebaseApp; private Supplier tokenFactory; private Supplier idTokenVerifier; private Supplier cookieVerifier; - private Supplier userManager; + private Supplier userManager; - private Builder() {} + protected abstract T getThis(); + + public FirebaseApp getFirebaseApp() { + return firebaseApp; + } - Builder setFirebaseApp(FirebaseApp firebaseApp) { + public T setFirebaseApp(FirebaseApp firebaseApp) { this.firebaseApp = firebaseApp; - return this; + return getThis(); } - Builder setTokenFactory(Supplier tokenFactory) { + public T setTokenFactory(Supplier tokenFactory) { this.tokenFactory = tokenFactory; - return this; + return getThis(); } - Builder setIdTokenVerifier(Supplier idTokenVerifier) { + public T setIdTokenVerifier(Supplier idTokenVerifier) { this.idTokenVerifier = idTokenVerifier; - return this; + return getThis(); } - Builder setCookieVerifier(Supplier cookieVerifier) { + public T setCookieVerifier(Supplier cookieVerifier) { this.cookieVerifier = cookieVerifier; - return this; + return getThis(); } - Builder setUserManager(Supplier userManager) { + public T setUserManager(Supplier userManager) { this.userManager = userManager; - return this; + return getThis(); } } + + protected static > T populateBuilderFromApp( + Builder builder, final FirebaseApp app, @Nullable final String tenantId) { + return builder.setFirebaseApp(app) + .setTokenFactory( + new Supplier() { + @Override + public FirebaseTokenFactory get() { + return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId); + } + }) + .setIdTokenVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId); + } + }) + .setCookieVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM, tenantId); + } + }) + .setUserManager( + new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager + .builder() + .setFirebaseApp(app) + .setTenantId(tenantId) + .build(); + } + }); + } } diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index f44bd2343..417ea6436 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -16,21 +16,11 @@ package com.google.firebase.auth; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.api.client.util.Clock; -import com.google.api.core.ApiFuture; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.auth.internal.FirebaseTokenFactory; import com.google.firebase.auth.multitenancy.TenantManager; -import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseService; -import com.google.firebase.internal.NonNull; /** * This class is the entry point for all server-side Firebase Authentication actions. @@ -46,14 +36,9 @@ public final class FirebaseAuth extends AbstractFirebaseAuth { private final Supplier tenantManager; - FirebaseAuth(final Builder builder) { + private FirebaseAuth(final Builder builder) { super(builder); - tenantManager = threadSafeMemoize(new Supplier() { - @Override - public TenantManager get() { - return new TenantManager(builder.firebaseApp); - } - }); + tenantManager = threadSafeMemoize(builder.tenantManager); } public TenantManager getTenantManager() { @@ -84,167 +69,18 @@ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { return service.getInstance(); } - /** - * Creates a new Firebase session cookie from the given ID token and options. The returned JWT can - * be set as a server-side session cookie with a custom cookie policy. - * - * @param idToken The Firebase ID token to exchange for a session cookie. - * @param options Additional options required to create the cookie. - * @return A Firebase session cookie string. - * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. - * @throws FirebaseAuthException If an error occurs while generating the session cookie. - */ - public String createSessionCookie(@NonNull String idToken, @NonNull SessionCookieOptions options) - throws FirebaseAuthException { - return createSessionCookieOp(idToken, options).call(); - } - - /** - * Similar to {@link #createSessionCookie(String, SessionCookieOptions)} but performs the - * operation asynchronously. - * - * @param idToken The Firebase ID token to exchange for a session cookie. - * @param options Additional options required to create the cookie. - * @return An {@code ApiFuture} which will complete successfully with a session cookie string. If - * an error occurs while generating the cookie or if the specified ID token is invalid, the - * future throws a {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. - */ - public ApiFuture createSessionCookieAsync( - @NonNull String idToken, @NonNull SessionCookieOptions options) { - return createSessionCookieOp(idToken, options).callAsync(getFirebaseApp()); - } - - private CallableOperation createSessionCookieOp( - final String idToken, final SessionCookieOptions options) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty"); - checkNotNull(options, "options must not be null"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected String execute() throws FirebaseAuthException { - return userManager.createSessionCookie(idToken, options); - } - }; - } - - /** - * Parses and verifies a Firebase session cookie. - * - *

      If verified successfully, returns a parsed version of the cookie from which the UID and the - * other claims can be read. If the cookie is invalid, throws a {@link FirebaseAuthException}. - * - *

      This method does not check whether the cookie has been revoked. See {@link - * #verifySessionCookie(String, boolean)}. - * - * @param cookie A Firebase session cookie string to verify and parse. - * @return A {@link FirebaseToken} representing the verified and decoded cookie. - */ - public FirebaseToken verifySessionCookie(String cookie) throws FirebaseAuthException { - return verifySessionCookie(cookie, false); - } - - /** - * Parses and verifies a Firebase session cookie. - * - *

      If {@code checkRevoked} is true, additionally verifies that the cookie has not been revoked. - * - *

      If verified successfully, returns a parsed version of the cookie from which the UID and the - * other claims can be read. If the cookie is invalid or has been revoked while {@code - * checkRevoked} is true, throws a {@link FirebaseAuthException}. - * - * @param cookie A Firebase session cookie string to verify and parse. - * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. - * @return A {@link FirebaseToken} representing the verified and decoded cookie. - */ - public FirebaseToken verifySessionCookie(String cookie, boolean checkRevoked) - throws FirebaseAuthException { - return verifySessionCookieOp(cookie, checkRevoked).call(); - } - - /** - * Similar to {@link #verifySessionCookie(String)} but performs the operation asynchronously. - * - * @param cookie A Firebase session cookie string to verify and parse. - * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or - * unsuccessfully with the failure Exception. - */ - public ApiFuture verifySessionCookieAsync(String cookie) { - return verifySessionCookieAsync(cookie, false); - } - - /** - * Similar to {@link #verifySessionCookie(String, boolean)} but performs the operation - * asynchronously. - * - * @param cookie A Firebase session cookie string to verify and parse. - * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. - * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or - * unsuccessfully with the failure Exception. - */ - public ApiFuture verifySessionCookieAsync(String cookie, boolean checkRevoked) { - return verifySessionCookieOp(cookie, checkRevoked).callAsync(getFirebaseApp()); - } - - private CallableOperation verifySessionCookieOp( - final String cookie, final boolean checkRevoked) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty"); - final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked); - return new CallableOperation() { - @Override - public FirebaseToken execute() throws FirebaseAuthException { - return sessionCookieVerifier.verifyToken(cookie); - } - }; - } - - @VisibleForTesting - FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) { - FirebaseTokenVerifier verifier = getCookieVerifier(); - if (checkRevoked) { - FirebaseUserManager userManager = getUserManager(); - verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager); - } - return verifier; - } - @Override protected void doDestroy() { } private static FirebaseAuth fromApp(final FirebaseApp app) { - return new FirebaseAuth( - AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setTokenFactory( - new Supplier() { - @Override - public FirebaseTokenFactory get() { - return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM); - } - }) - .setIdTokenVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM); - } - }) - .setCookieVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); - } - }) - .setUserManager( - new Supplier() { - @Override - public FirebaseUserManager get() { - return FirebaseUserManager.builder().setFirebaseApp(app).build(); - } - })); + return populateBuilderFromApp(builder(), app, null) + .setTenantManager(new Supplier() { + @Override + public TenantManager get() { + return new TenantManager(app); + } + }) + .build(); } private static class FirebaseAuthService extends FirebaseService { @@ -258,4 +94,29 @@ public void destroy() { instance.destroy(); } } + + static Builder builder() { + return new Builder(); + } + + static class Builder extends AbstractFirebaseAuth.Builder { + + private Supplier tenantManager; + + private Builder() { } + + @Override + protected Builder getThis() { + return this; + } + + public Builder setTenantManager(Supplier tenantManager) { + this.tenantManager = tenantManager; + return this; + } + + public FirebaseAuth build() { + return new FirebaseAuth(this); + } + } } diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java index e0105e9aa..7c8f9f0a5 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java @@ -99,6 +99,11 @@ static FirebaseTokenVerifierImpl createIdTokenVerifier( } static FirebaseTokenVerifierImpl createSessionCookieVerifier(FirebaseApp app, Clock clock) { + return createSessionCookieVerifier(app, clock, null); + } + + static FirebaseTokenVerifierImpl createSessionCookieVerifier( + FirebaseApp app, Clock clock, @Nullable String tenantId) { String projectId = ImplFirebaseTrampolines.getProjectId(app); checkState(!Strings.isNullOrEmpty(projectId), "Must initialize FirebaseApp with a project ID to call verifySessionCookie()"); @@ -107,12 +112,13 @@ static FirebaseTokenVerifierImpl createSessionCookieVerifier(FirebaseApp app, Cl GooglePublicKeysManager publicKeysManager = newPublicKeysManager( app.getOptions(), clock, SESSION_COOKIE_CERT_URL); return FirebaseTokenVerifierImpl.builder() - .setJsonFactory(app.getOptions().getJsonFactory()) - .setPublicKeysManager(publicKeysManager) - .setIdTokenVerifier(idTokenVerifier) .setShortName("session cookie") .setMethod("verifySessionCookie()") .setDocUrl("https://firebase.google.com/docs/auth/admin/manage-cookies") + .setJsonFactory(app.getOptions().getJsonFactory()) + .setPublicKeysManager(publicKeysManager) + .setIdTokenVerifier(idTokenVerifier) + .setTenantId(tenantId) .build(); } diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java index 540437404..95ef169da 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java @@ -18,9 +18,16 @@ import static com.google.common.base.Preconditions.checkArgument; +import com.google.api.core.ApiAsyncFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; import com.google.common.base.Strings; +import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.FirebaseApp; import com.google.firebase.auth.AbstractFirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseToken; +import com.google.firebase.auth.SessionCookieOptions; /** * The tenant-aware Firebase client. @@ -32,10 +39,10 @@ public final class TenantAwareFirebaseAuth extends AbstractFirebaseAuth { private final String tenantId; - TenantAwareFirebaseAuth(final FirebaseApp firebaseApp, final String tenantId) { - super(builderFromAppAndTenantId(firebaseApp, tenantId)); - checkArgument(!Strings.isNullOrEmpty(tenantId)); - this.tenantId = tenantId; + private TenantAwareFirebaseAuth(Builder builder) { + super(builder); + checkArgument(!Strings.isNullOrEmpty(builder.tenantId)); + this.tenantId = builder.tenantId; } /** Returns the client's tenant ID. */ @@ -43,8 +50,58 @@ public String getTenantId() { return tenantId; } + @Override + public String createSessionCookie( + String idToken, SessionCookieOptions options) throws FirebaseAuthException { + verifyIdToken(idToken); + return super.createSessionCookie(idToken, options); + } + + @Override + public ApiFuture createSessionCookieAsync( + final String idToken, final SessionCookieOptions options) { + ApiFuture future = verifyIdTokenAsync(idToken); + return ApiFutures.transformAsync(future, new ApiAsyncFunction() { + @Override + public ApiFuture apply(FirebaseToken input) { + return TenantAwareFirebaseAuth.super.createSessionCookieAsync(idToken, options); + } + }, MoreExecutors.directExecutor()); + } + @Override protected void doDestroy() { // Nothing extra needs to be destroyed. } + + static TenantAwareFirebaseAuth fromApp(FirebaseApp app, String tenantId) { + return populateBuilderFromApp(builder(), app, tenantId) + .setTenantId(tenantId) + .build(); + } + + static Builder builder() { + return new Builder(); + } + + static class Builder extends AbstractFirebaseAuth.Builder { + + private String tenantId; + + private Builder() { } + + @Override + protected Builder getThis() { + return this; + } + + public Builder setTenantId(String tenantId) { + this.tenantId = tenantId; + return this; + } + + TenantAwareFirebaseAuth build() { + return new TenantAwareFirebaseAuth(this); + } + } } diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java index 11f26b096..dcb226d28 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java @@ -79,7 +79,7 @@ public Tenant getTenant(@NonNull String tenantId) throws FirebaseAuthException { public synchronized TenantAwareFirebaseAuth getAuthForTenant(@NonNull String tenantId) { checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); if (!tenantAwareAuths.containsKey(tenantId)) { - tenantAwareAuths.put(tenantId, new TenantAwareFirebaseAuth(firebaseApp, tenantId)); + tenantAwareAuths.put(tenantId, TenantAwareFirebaseAuth.fromApp(firebaseApp, tenantId)); } return tenantAwareAuths.get(tenantId); } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index 635e1bfba..bfc8d1790 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -20,6 +20,7 @@ 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; @@ -28,11 +29,11 @@ import com.google.common.base.Defaults; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; -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.testing.ServiceAccount; +import com.google.firebase.testing.TestResponseInterceptor; import com.google.firebase.testing.TestUtils; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -43,7 +44,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; - import org.junit.After; import org.junit.Test; @@ -169,8 +169,7 @@ public void testDefaultIdTokenVerifier() { @Test public void testIdTokenVerifierInitializedOnDemand() throws Exception { - FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("idTokenUser")); + FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("idTokenUser"); CountingSupplier countingSupplier = new CountingSupplier<>( Suppliers.ofInstance(tokenVerifier)); @@ -185,37 +184,33 @@ public void testIdTokenVerifierInitializedOnDemand() throws Exception { @Test public void testVerifyIdTokenWithNull() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); - tokenVerifier.lastTokenString = "_init_"; + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { auth.verifyIdTokenAsync(null); fail("No error thrown for null id token"); } catch (IllegalArgumentException expected) { - assertEquals("_init_", tokenVerifier.getLastTokenString()); + assertNull(tokenVerifier.getLastTokenString()); } } @Test public void testVerifyIdTokenWithEmptyString() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); - tokenVerifier.lastTokenString = "_init_"; + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { auth.verifyIdTokenAsync(""); fail("No error thrown for null id token"); } catch (IllegalArgumentException expected) { - assertEquals("_init_", tokenVerifier.getLastTokenString()); + assertNull(tokenVerifier.getLastTokenString()); } - } @Test public void testVerifyIdToken() throws Exception { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("testUser")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser"); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); FirebaseToken firebaseToken = auth.verifyIdToken("idtoken"); @@ -242,8 +237,7 @@ public void testVerifyIdTokenFailure() { @Test public void testVerifyIdTokenAsync() throws Exception { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("testUser")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser"); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); FirebaseToken firebaseToken = auth.verifyIdTokenAsync("idtoken").get(); @@ -300,8 +294,7 @@ public void testDefaultSessionCookieVerifier() { @Test public void testSessionCookieVerifierInitializedOnDemand() throws Exception { - FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("cookieUser")); + FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("cookieUser"); CountingSupplier countingSupplier = new CountingSupplier<>( Suppliers.ofInstance(tokenVerifier)); FirebaseAuth auth = getAuthForSessionCookieVerification(countingSupplier); @@ -316,37 +309,33 @@ public void testSessionCookieVerifierInitializedOnDemand() throws Exception { @Test public void testVerifySessionCookieWithNull() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); - tokenVerifier.lastTokenString = "_init_"; + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { auth.verifySessionCookieAsync(null); fail("No error thrown for null id token"); } catch (IllegalArgumentException expected) { - assertEquals("_init_", tokenVerifier.getLastTokenString()); + assertNull(tokenVerifier.getLastTokenString()); } } @Test public void testVerifySessionCookieWithEmptyString() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); - tokenVerifier.lastTokenString = "_init_"; + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { auth.verifySessionCookieAsync(""); fail("No error thrown for null id token"); } catch (IllegalArgumentException expected) { - assertEquals("_init_", tokenVerifier.getLastTokenString()); + assertNull(tokenVerifier.getLastTokenString()); } - } @Test public void testVerifySessionCookie() throws Exception { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("testUser")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser"); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); FirebaseToken firebaseToken = auth.verifySessionCookie("idtoken"); @@ -373,8 +362,7 @@ public void testVerifySessionCookieFailure() { @Test public void testVerifySessionCookieAsync() throws Exception { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("testUser")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser"); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); FirebaseToken firebaseToken = auth.verifySessionCookieAsync("idtoken").get(); @@ -417,10 +405,6 @@ public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws Interru } } - private FirebaseToken getFirebaseToken(String subject) { - return new FirebaseToken(ImmutableMap.of("sub", subject)); - } - private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVerifier) { return getAuthForIdTokenVerification(Suppliers.ofInstance(tokenVerifier)); } @@ -429,11 +413,11 @@ private FirebaseAuth getAuthForIdTokenVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); - return new FirebaseAuth( - AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setIdTokenVerifier(tokenVerifierSupplier) - .setUserManager(Suppliers.ofInstance(userManager))); + return FirebaseAuth.builder() + .setFirebaseApp(app) + .setIdTokenVerifier(tokenVerifierSupplier) + .setUserManager(Suppliers.ofInstance(userManager)) + .build(); } private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) { @@ -444,45 +428,23 @@ private FirebaseAuth getAuthForSessionCookieVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); - return new FirebaseAuth( - AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setCookieVerifier(tokenVerifierSupplier) - .setUserManager(Suppliers.ofInstance(userManager))); + return FirebaseAuth.builder() + .setFirebaseApp(app) + .setCookieVerifier(tokenVerifierSupplier) + .setUserManager(Suppliers.ofInstance(userManager)) + .build(); } - private static class MockTokenVerifier implements FirebaseTokenVerifier { - - private String lastTokenString; - - private FirebaseToken result; - private FirebaseAuthException exception; - - private MockTokenVerifier(FirebaseToken result, FirebaseAuthException exception) { - this.result = result; - this.exception = exception; - } - - @Override - public FirebaseToken verifyToken(String token) throws FirebaseAuthException { - lastTokenString = token; - if (exception != null) { - throw exception; - } - return result; - } - - String getLastTokenString() { - return this.lastTokenString; - } - - static MockTokenVerifier fromResult(FirebaseToken result) { - return new MockTokenVerifier(result, null); - } - - static MockTokenVerifier fromException(FirebaseAuthException exception) { - return new MockTokenVerifier(null, exception); - } + public static TestResponseInterceptor setUserManager( + AbstractFirebaseAuth.Builder builder, FirebaseApp app, String tenantId) { + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseUserManager userManager = FirebaseUserManager.builder() + .setFirebaseApp(app) + .setTenantId(tenantId) + .build(); + userManager.setInterceptor(interceptor); + builder.setUserManager(Suppliers.ofInstance(userManager)); + return interceptor; } private static class CountingSupplier implements Supplier { diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java index 5dd1e9c14..379da0a57 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -217,6 +217,19 @@ public void testMalformedToken() throws Exception { tokenVerifier.verifyToken("not.a.jwt"); } + @Test + public void testVerifyTokenWithTenantId() throws FirebaseAuthException { + FirebaseTokenVerifierImpl verifier = fullyPopulatedBuilder() + .setTenantId("TENANT_1") + .build(); + + FirebaseToken firebaseToken = verifier.verifyToken(createTokenWithTenantId("TENANT_1")); + + assertEquals(TEST_TOKEN_ISSUER, firebaseToken.getIssuer()); + assertEquals(TestTokenFactory.UID, firebaseToken.getUid()); + assertEquals("TENANT_1", firebaseToken.getTenantId()); + } + @Test public void testVerifyTokenDifferentTenantIds() { try { diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index d407fd8ec..7012ef6bf 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -2602,19 +2602,18 @@ private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse respon .setProjectId("test-project-id") .setHttpTransport(transport) .build()); - return new FirebaseAuth( - AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setUserManager(new Supplier() { - @Override - public FirebaseUserManager get() { - return FirebaseUserManager - .builder() - .setFirebaseApp(app) - .setHttpRequestFactory(transport.createRequestFactory()) - .build(); - } - })); + return FirebaseAuth.builder() + .setFirebaseApp(app) + .setUserManager(new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager.builder() + .setFirebaseApp(app) + .setHttpRequestFactory(transport.createRequestFactory()) + .build(); + } + }) + .build(); } private static void checkUserRecord(UserRecord userRecord) { diff --git a/src/test/java/com/google/firebase/auth/MockTokenVerifier.java b/src/test/java/com/google/firebase/auth/MockTokenVerifier.java new file mode 100644 index 000000000..51f7d5a47 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/MockTokenVerifier.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import com.google.common.collect.ImmutableMap; +import java.util.concurrent.atomic.AtomicReference; + +public class MockTokenVerifier implements FirebaseTokenVerifier { + + private final FirebaseToken result; + private final FirebaseAuthException exception; + private final AtomicReference lastTokenString = new AtomicReference<>(); + + private MockTokenVerifier(FirebaseToken result, FirebaseAuthException exception) { + this.result = result; + this.exception = exception; + } + + @Override + public FirebaseToken verifyToken(String token) throws FirebaseAuthException { + lastTokenString.set(token); + if (exception != null) { + throw exception; + } + return result; + } + + public String getLastTokenString() { + return this.lastTokenString.get(); + } + + public static MockTokenVerifier fromUid(String uid) { + long iat = System.currentTimeMillis() / 1000; + return fromResult(new FirebaseToken(ImmutableMap.of( + "sub", uid, "iat", iat))); + } + + public static MockTokenVerifier fromResult(FirebaseToken result) { + return new MockTokenVerifier(result, null); + } + + public static MockTokenVerifier fromException(FirebaseAuthException exception) { + return new MockTokenVerifier(null, exception); + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java new file mode 100644 index 000000000..df7bcf00d --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java @@ -0,0 +1,292 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +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.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.json.GenericJson; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.common.base.Suppliers; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseAuthTest; +import com.google.firebase.auth.FirebaseToken; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.auth.MockTokenVerifier; +import com.google.firebase.auth.SessionCookieOptions; +import com.google.firebase.testing.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Test; + +public class TenantAwareFirebaseAuthTest { + + private static final String TENANT_ID = "test-tenant"; + + private static final String AUTH_BASE_URL = "/v1/projects/test-project-id/tenants/" + TENANT_ID; + + private static final SessionCookieOptions COOKIE_OPTIONS = SessionCookieOptions.builder() + .setExpiresIn(TimeUnit.HOURS.toMillis(1)) + .build(); + + private static final FirebaseAuthException AUTH_EXCEPTION = new FirebaseAuthException( + "code", "reason"); + + private static final String CREATE_COOKIE_RESPONSE = TestUtils.loadResource( + "createSessionCookie.json"); + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testCreateSessionCookieAsync() throws Exception { + MockTokenVerifier verifier = MockTokenVerifier.fromUid("uid"); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(verifier); + TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE); + TenantAwareFirebaseAuth auth = builder.build(); + + String cookie = auth.createSessionCookieAsync("testToken", COOKIE_OPTIONS).get(); + + assertEquals("MockCookieString", cookie); + assertEquals("testToken", verifier.getLastTokenString()); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(2, parsed.size()); + assertEquals("testToken", parsed.get("idToken")); + assertEquals(new BigDecimal(3600), parsed.get("validDuration")); + checkUrl(interceptor, AUTH_BASE_URL + ":createSessionCookie"); + } + + @Test + public void testCreateSessionCookieAsyncError() throws Exception { + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification( + MockTokenVerifier.fromException(AUTH_EXCEPTION)); + TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.createSessionCookieAsync("testToken", COOKIE_OPTIONS).get(); + fail("No error thrown for invalid ID token"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + FirebaseAuthException cause = (FirebaseAuthException) e.getCause(); + assertEquals("code", cause.getErrorCode()); + } + + assertNull(interceptor.getResponse()); + } + + @Test + public void testCreateSessionCookie() throws Exception { + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification( + MockTokenVerifier.fromUid("uid")); + TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE); + TenantAwareFirebaseAuth auth = builder.build(); + + String cookie = auth.createSessionCookie("testToken", COOKIE_OPTIONS); + + assertEquals("MockCookieString", cookie); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(2, parsed.size()); + assertEquals("testToken", parsed.get("idToken")); + assertEquals(new BigDecimal(3600), parsed.get("validDuration")); + checkUrl(interceptor, AUTH_BASE_URL + ":createSessionCookie"); + } + + @Test + public void testCreateSessionCookieError() { + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification( + MockTokenVerifier.fromException(AUTH_EXCEPTION)); + TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.createSessionCookie("testToken", COOKIE_OPTIONS); + fail("No error thrown for invalid ID token"); + } catch (FirebaseAuthException e) { + assertEquals("code", e.getErrorCode()); + } + + assertNull(interceptor.getResponse()); + } + + @Test + public void testVerifySessionCookie() throws FirebaseAuthException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + FirebaseToken token = auth.verifySessionCookie("cookie"); + + assertEquals("uid", token.getUid()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifySessionCookieFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.verifySessionCookie("cookie"); + fail("No error thrown for invalid token"); + } catch (FirebaseAuthException authException) { + assertEquals("code", authException.getErrorCode()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifySessionCookieAsync() throws Exception { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + FirebaseToken firebaseToken = auth.verifySessionCookieAsync("cookie").get(); + + assertEquals("uid", firebaseToken.getUid()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifySessionCookieAsyncFailure() throws InterruptedException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.verifySessionCookieAsync("cookie").get(); + fail("No error thrown for invalid token"); + } catch (ExecutionException e) { + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals("code", authException.getErrorCode()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifySessionCookieWithCheckRevoked() throws FirebaseAuthException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + TestResponseInterceptor interceptor = setUserManager( + builder, TestUtils.loadResource("getUser.json")); + TenantAwareFirebaseAuth auth = builder.build(); + + FirebaseToken token = auth.verifySessionCookie("cookie", true); + + assertEquals("uid", token.getUid()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + checkUrl(interceptor, AUTH_BASE_URL + "/accounts:lookup"); + } + + @Test + public void testVerifySessionCookieWithCheckRevokedFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.verifySessionCookie("cookie", true); + fail("No error thrown for invalid token"); + } catch (FirebaseAuthException e) { + assertEquals("code", e.getErrorCode()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws InterruptedException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.verifySessionCookieAsync("cookie", true).get(); + fail("No error thrown for invalid token"); + } catch (ExecutionException e) { + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals("code", authException.getErrorCode()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + } + + private static TenantAwareFirebaseAuth.Builder builderForTokenVerification( + MockTokenVerifier verifier) { + return TenantAwareFirebaseAuth.builder() + .setTenantId(TENANT_ID) + .setIdTokenVerifier(Suppliers.ofInstance(verifier)) + .setCookieVerifier(Suppliers.ofInstance(verifier)); + } + + private static TestResponseInterceptor setUserManager(TenantAwareFirebaseAuth.Builder builder) { + return setUserManager(builder, "{}"); + } + + private static TestResponseInterceptor setUserManager( + TenantAwareFirebaseAuth.Builder builder, String response) { + FirebaseApp app = initializeAppWithResponse(response); + builder.setFirebaseApp(app); + return FirebaseAuthTest.setUserManager(builder, app, TENANT_ID); + } + + private static FirebaseApp initializeAppWithResponse(String response) { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent(response)) + .build(); + return FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("token")) + .setHttpTransport(transport) + .setProjectId("test-project-id") + .build()); + } + + private static GenericJson parseRequestContent(TestResponseInterceptor interceptor) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + return Utils.getDefaultJsonFactory().fromString( + new String(out.toByteArray()), GenericJson.class); + } + + private static void checkUrl(TestResponseInterceptor interceptor, String url) { + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(HttpMethods.POST, request.getRequestMethod()); + assertEquals(url, request.getUrl().getRawPath()); + } +} From 8c86411a5acef67906699bb543df52161ee93a0a Mon Sep 17 00:00:00 2001 From: shambhand <33409682+shambhand@users.noreply.github.com> Date: Tue, 11 Aug 2020 01:22:29 +0530 Subject: [PATCH 127/456] fix(fcm): Added toString() in TopicManagementResponse for logging (#469) * Added toString() in TopicManagementResponse for logging (#343) * Used MoreObjects.toStringHelper() * removed .DS_Store & added it to .gitignore * unit test case for TopicManagementResponse toString() functionality added for logging * fixed checkStyle issue of line should not greater than 100 char --- .gitignore | 1 + .../firebase/messaging/TopicManagementResponse.java | 9 +++++++++ .../firebase/messaging/InstanceIdClientImplTest.java | 11 +++++++++++ 3 files changed, 21 insertions(+) diff --git a/.gitignore b/.gitignore index a869aa65d..6e7d29cfd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ target/ release.properties integration_cert.json integration_apikey.txt +.DS_Store diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java index b8f92576e..bbf4c944a 100644 --- a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java +++ b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.api.client.json.GenericJson; +import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.internal.NonNull; @@ -122,5 +123,13 @@ public int getIndex() { public String getReason() { return reason; } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("index", index) + .add("reason", reason) + .toString(); + } } } diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java index f939c8382..c7e1cc24a 100644 --- a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java @@ -340,6 +340,17 @@ public void testTopicManagementResponseWithEmptyList() { new TopicManagementResponse(ImmutableList.of()); } + @Test + public void testTopicManagementResponseErrorToString() { + GenericJson json = new GenericJson().set("error", "test error"); + ImmutableList jsonList = ImmutableList.of(json); + + TopicManagementResponse topicManagementResponse = new TopicManagementResponse(jsonList); + + String expected = "[Error{index=0, reason=unknown-error}]"; + assertEquals(expected, topicManagementResponse.getErrors().toString()); + } + private static InstanceIdClientImpl initInstanceIdClient( final MockLowLevelHttpResponse mockResponse, final HttpResponseInterceptor interceptor) { From e72bf8abb3fb7b681845f6d2cc0cdea5f80af61b Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 11 Aug 2020 16:23:20 -0400 Subject: [PATCH 128/456] Fix javadoc error caused by having protected parameters public methods (#470) --- .../google/firebase/auth/AbstractFirebaseAuth.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 6c841070c..7061d2b44 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -1782,11 +1782,6 @@ public T setFirebaseApp(FirebaseApp firebaseApp) { return getThis(); } - public T setTokenFactory(Supplier tokenFactory) { - this.tokenFactory = tokenFactory; - return getThis(); - } - public T setIdTokenVerifier(Supplier idTokenVerifier) { this.idTokenVerifier = idTokenVerifier; return getThis(); @@ -1797,10 +1792,15 @@ public T setCookieVerifier(Supplier cookieVerif return getThis(); } - public T setUserManager(Supplier userManager) { + T setUserManager(Supplier userManager) { this.userManager = userManager; return getThis(); } + + T setTokenFactory(Supplier tokenFactory) { + this.tokenFactory = tokenFactory; + return getThis(); + } } protected static > T populateBuilderFromApp( From 36eb4eaae143e808c529659b72c0aa234c8a4678 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 12 Aug 2020 13:46:18 -0400 Subject: [PATCH 129/456] [chore] Release 6.16.0 (#471) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b0187543a..c2dc9f336 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.15.0 + 6.16.0 jar firebase-admin From cc520a721ef3ced13c6c1e34fa010d082a948b99 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 18 Aug 2020 10:24:17 -0700 Subject: [PATCH 130/456] change: New error handling APIs (merging v7 branch into master) (#465) * Added core types for the error handling revamp (#350) * Added core types for the error handling revamp * Fixed copyright year * Added ErrorHandlingHttpClient API (#353) * Added core error handling abstractions * Added unit tests for the new functionality * Exposed getErrorCode as getErrorCodeNew * Enabled error code assertions * Error handling revamp for the FirebaseMessaging API (#357) * Added core error handling abstractions * Added unit tests for the new functionality * Exposed getErrorCode as getErrorCodeNew * Enabled error code assertions * Error handling revamp for FCM APIs * Cleaned up the FirebaseMessagingException * Cleaned up the AbstractHttpErrorHandler class * Updated tests * Error handling revamp for FirebaseInstanceId API (#359) * Delete instance ID API error handling revamp * Added tests for IO and parse errors * fix(auth): Migrated user management APIs to the new error handling scheme (#360) * Error handling revamp in FirebaseUserManager * Updated integration tests; Added documentation * Assigning the correct ErrorCode for auth errors * Moved AuthErrorHandler to a separate top-level class * Error handling revamp for token verification APIs (#362) * Error handling revamp for token verification APIs * Updated javadocs * Error handling revamp for the custom token creation API (#366) * Error handlign revamp for the custom token creation API * Using the correct authorized HTTP client for IAM requests * Error handling revamp for the project management API (#367) * Error handling revamp for the project management API * Minor code and test cleanup * Fixed some lint errors; Removed requestFactory reference from project mgt service impl * Renamed getErrorCodeNew() to getErrorCode() (#379) * Minor code and test cleanup * Renamed getErrorCodeNew() to getErrorCode() in FirebaseException * Fixing checkstyle error * Fixing some deprecation warnings (#380) * Handling IID error codes correctly (#381) * Removed old deprecated APIs (#383) * fix: Removed unused FirebaseAppStore abstraction (#427) * fix: Removed unused FirebaseAppStore abstraction * Using the keySet of App instances to populate the app names list * chore: Removing redundant test dependency (#441) * chore: Make user import hashing classes final (#425) * chore: Merged with v7 branch with master (#456) * fix(fcm): Replacing deprecated Batch API constructor (#460) * fix: Handling http method override in ErrorHandlingHttpClient (#459) * fix: Handling JSON serialization/response interception at ErrorHandlingHttpClient (#462) * fix: Handling JSON serialization and response interception at ErrorHandlingHttpClient * fix: Removing redundant method override header * feat: Added new error codes for IdP management and multitenancy (#458) * feat: Added new error codes for IdP management and multitenancy * fix: Updated integration tests * fix: Renamed helper method * fix: Removing some calls to deprecated APIs (#464) * chore: Support for specifying query parameters in HttpRequestInfo (#463) * chore: Support for specifying query parameters in HttpRequestInfo * fix: Removing redundant JsonObjectParser from HttpClient * fix: Fixing a series of javadoc warnings (#466) * fix: Made some APIs on AbstractFirebaseAuth.Builder package-protected for consistency * Apply suggestions from code review Co-authored-by: egilmorez Co-authored-by: Kevin Cheung * fix: Minor updates to API ref docs based on code review comments * fix: Fixing API doc wording Co-authored-by: Horatiu Lazu Co-authored-by: egilmorez Co-authored-by: Kevin Cheung --- pom.xml | 6 - .../java/com/google/firebase/ErrorCode.java | 109 +++++ .../java/com/google/firebase/FirebaseApp.java | 42 +- .../FirebaseAppLifecycleListener.java | 2 +- .../google/firebase/FirebaseException.java | 51 ++- .../com/google/firebase/FirebaseOptions.java | 20 +- .../google/firebase/IncomingHttpResponse.java | 114 +++++ .../google/firebase/OutgoingHttpRequest.java | 98 ++++ .../firebase/auth/AbstractFirebaseAuth.java | 26 +- .../google/firebase/auth/AuthErrorCode.java | 104 +++++ .../firebase/auth/FirebaseAuthException.java | 36 +- .../firebase/auth/FirebaseTokenUtils.java | 4 + .../auth/FirebaseTokenVerifierImpl.java | 131 ++++-- .../firebase/auth/FirebaseUserManager.java | 230 ++++------ .../auth/ListProviderConfigsPage.java | 20 +- .../firebase/auth/OidcProviderConfig.java | 2 +- .../auth/RevocationCheckDecorator.java | 23 +- .../com/google/firebase/auth/hash/Bcrypt.java | 2 +- .../google/firebase/auth/hash/HmacMd5.java | 2 +- .../google/firebase/auth/hash/HmacSha1.java | 2 +- .../google/firebase/auth/hash/HmacSha256.java | 2 +- .../google/firebase/auth/hash/HmacSha512.java | 2 +- .../com/google/firebase/auth/hash/Md5.java | 2 +- .../firebase/auth/hash/Pbkdf2Sha256.java | 2 +- .../google/firebase/auth/hash/PbkdfSha1.java | 2 +- .../com/google/firebase/auth/hash/Sha1.java | 2 +- .../com/google/firebase/auth/hash/Sha256.java | 2 +- .../com/google/firebase/auth/hash/Sha512.java | 2 +- .../firebase/auth/hash/StandardScrypt.java | 2 +- .../auth/internal/AuthErrorHandler.java | 222 +++++++++ .../auth/internal/AuthHttpClient.java | 111 +---- .../firebase/auth/internal/CryptoSigner.java | 5 +- .../firebase/auth/internal/CryptoSigners.java | 90 ++-- .../auth/internal/FirebaseTokenFactory.java | 38 +- .../auth/internal/HttpErrorResponse.java | 49 -- .../multitenancy/FirebaseTenantClient.java | 46 +- .../auth/multitenancy/ListTenantsPage.java | 12 +- .../auth/multitenancy/TenantManager.java | 9 +- .../database/core/JvmAuthTokenProvider.java | 2 +- .../firebase/iid/FirebaseInstanceId.java | 101 +++-- .../iid/FirebaseInstanceIdException.java | 6 +- .../internal/AbstractHttpErrorHandler.java | 154 +++++++ .../AbstractPlatformErrorHandler.java | 98 ++++ .../firebase/internal/ApiClientUtils.java | 2 + .../internal/ErrorHandlingHttpClient.java | 144 ++++++ .../firebase/internal/FirebaseAppStore.java | 78 ---- .../firebase/internal/HttpErrorHandler.java | 44 ++ .../firebase/internal/HttpRequestInfo.java | 132 ++++++ .../firebase/messaging/FirebaseMessaging.java | 4 - .../FirebaseMessagingClientImpl.java | 255 ++++++----- .../messaging/FirebaseMessagingException.java | 48 +- .../messaging/InstanceIdClientImpl.java | 165 +++---- .../firebase/messaging/LightSettings.java | 4 +- .../messaging/MessagingErrorCode.java | 43 ++ .../firebase/messaging/Notification.java | 31 +- .../MessagingServiceErrorResponse.java | 37 +- .../FirebaseProjectManagementException.java | 21 +- .../FirebaseProjectManagementServiceImpl.java | 66 +-- .../projectmanagement/HttpHelper.java | 170 +++---- .../com/google/firebase/FirebaseAppTest.java | 33 +- .../firebase/FirebaseExceptionTest.java | 124 +++++ .../google/firebase/FirebaseOptionsTest.java | 39 +- .../firebase/IncomingHttpResponseTest.java | 141 ++++++ .../firebase/OutgoingHttpRequestTest.java | 89 ++++ .../google/firebase/ThreadManagerTest.java | 7 +- .../google/firebase/auth/FirebaseAuthIT.java | 121 +++-- .../firebase/auth/FirebaseAuthTest.java | 177 ++++++-- .../auth/FirebaseTokenVerifierImplTest.java | 244 +++++++--- .../auth/FirebaseUserManagerTest.java | 423 ++++++++++++------ .../auth/ProviderConfigTestUtils.java | 12 +- .../google/firebase/auth/UserTestUtils.java | 4 +- .../auth/internal/CryptoSignersTest.java | 54 ++- .../internal/FirebaseTokenFactoryTest.java | 7 +- .../FirebaseTenantClientTest.java | 91 ++-- .../TenantAwareFirebaseAuthIT.java | 5 +- .../TenantAwareFirebaseAuthTest.java | 15 +- .../auth/multitenancy/TenantManagerIT.java | 8 +- .../firebase/cloud/FirestoreClientTest.java | 15 +- .../firebase/cloud/StorageClientTest.java | 10 +- .../firebase/database/DataSnapshotTest.java | 2 +- .../database/DatabaseReferenceTest.java | 2 +- .../database/FirebaseDatabaseTest.java | 4 +- .../google/firebase/database/TestHelpers.java | 4 +- .../database/ValueExpectationHelper.java | 4 +- .../collection/RBTreeSortedMapTest.java | 18 +- .../core/JvmAuthTokenProviderTest.java | 28 +- .../database/core/JvmPlatformTest.java | 4 +- .../firebase/database/core/RepoTest.java | 2 +- .../firebase/database/core/SyncPointTest.java | 2 +- .../DefaultPersistenceManagerTest.java | 2 +- .../persistence/RandomPersistenceTest.java | 2 +- .../database/integration/DataTestIT.java | 4 +- .../FirebaseDatabaseAuthTestIT.java | 16 +- .../integration/FirebaseDatabaseTestIT.java | 4 +- .../database/integration/RulesTestIT.java | 2 +- .../database/integration/ShutdownExample.java | 2 +- .../database/utilities/ValidationTest.java | 38 +- .../firebase/iid/FirebaseInstanceIdTest.java | 170 +++++-- .../AbstractPlatformErrorHandlerTest.java | 298 ++++++++++++ .../internal/CallableOperationTest.java | 3 +- .../internal/ErrorHandlingHttpClientTest.java | 358 +++++++++++++++ .../internal/FirebaseAppStoreTest.java | 17 +- .../FirebaseRequestInitializerTest.java | 10 +- .../internal/FirebaseThreadManagersTest.java | 8 +- .../firebase/internal/TestApiClientUtils.java | 21 +- .../firebase/messaging/BatchResponseTest.java | 5 +- .../FirebaseMessagingClientImplTest.java | 137 ++++-- .../messaging/FirebaseMessagingIT.java | 46 +- .../messaging/FirebaseMessagingTest.java | 3 +- .../messaging/InstanceIdClientImplTest.java | 127 ++++-- .../firebase/messaging/MessageTest.java | 33 +- .../messaging/MulticastMessageTest.java | 5 +- .../firebase/messaging/SendResponseTest.java | 5 +- ...ebaseProjectManagementServiceImplTest.java | 182 ++++++-- .../FirebaseProjectManagementTest.java | 5 +- .../projectmanagement/IosAppTest.java | 3 +- .../snippets/FirebaseAppSnippets.java | 14 +- .../snippets/FirebaseAuthSnippets.java | 3 +- .../snippets/FirebaseDatabaseSnippets.java | 2 +- .../snippets/FirebaseMessagingSnippets.java | 24 +- .../snippets/FirebaseStorageSnippets.java | 2 +- .../firebase/testing/FirebaseAppRule.java | 2 - .../testing/IntegrationTestUtils.java | 3 +- .../google/firebase/testing/TestUtils.java | 10 +- 124 files changed, 4687 insertions(+), 1797 deletions(-) create mode 100644 src/main/java/com/google/firebase/ErrorCode.java create mode 100644 src/main/java/com/google/firebase/IncomingHttpResponse.java create mode 100644 src/main/java/com/google/firebase/OutgoingHttpRequest.java create mode 100644 src/main/java/com/google/firebase/auth/AuthErrorCode.java create mode 100644 src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java delete mode 100644 src/main/java/com/google/firebase/auth/internal/HttpErrorResponse.java create mode 100644 src/main/java/com/google/firebase/internal/AbstractHttpErrorHandler.java create mode 100644 src/main/java/com/google/firebase/internal/AbstractPlatformErrorHandler.java create mode 100644 src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java delete mode 100644 src/main/java/com/google/firebase/internal/FirebaseAppStore.java create mode 100644 src/main/java/com/google/firebase/internal/HttpErrorHandler.java create mode 100644 src/main/java/com/google/firebase/internal/HttpRequestInfo.java create mode 100644 src/main/java/com/google/firebase/messaging/MessagingErrorCode.java create mode 100644 src/test/java/com/google/firebase/FirebaseExceptionTest.java create mode 100644 src/test/java/com/google/firebase/IncomingHttpResponseTest.java create mode 100644 src/test/java/com/google/firebase/OutgoingHttpRequestTest.java create mode 100644 src/test/java/com/google/firebase/internal/AbstractPlatformErrorHandlerTest.java create mode 100644 src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java diff --git a/pom.xml b/pom.xml index c2dc9f336..a72301d5b 100644 --- a/pom.xml +++ b/pom.xml @@ -477,12 +477,6 @@ 0.6 test - - com.cedarsoftware - java-util - 1.26.0 - test - junit junit diff --git a/src/main/java/com/google/firebase/ErrorCode.java b/src/main/java/com/google/firebase/ErrorCode.java new file mode 100644 index 000000000..0eaa9cec2 --- /dev/null +++ b/src/main/java/com/google/firebase/ErrorCode.java @@ -0,0 +1,109 @@ +/* + * Copyright 2020 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; + +/** + * Platform-wide error codes that can be raised by Admin SDK APIs. + */ +public enum ErrorCode { + + /** + * Client specified an invalid argument. + */ + INVALID_ARGUMENT, + + /** + * Request cannot be executed in the current system state, such as deleting a non-empty + * directory. + */ + FAILED_PRECONDITION, + + /** + * Client specified an invalid range. + */ + OUT_OF_RANGE, + + /** + * Request not authenticated due to missing, invalid, or expired OAuth token. + */ + UNAUTHENTICATED, + + /** + * Client does not have sufficient permission. This can happen because the OAuth token does + * not have the right scopes, the client doesn't have permission, or the API has not been + * enabled for the client project. + */ + PERMISSION_DENIED, + + /** + * A specified resource is not found, or the request is rejected for unknown reasons, + * such as a blocked network address. + */ + NOT_FOUND, + + /** + * Concurrency conflict, such as read-modify-write conflict. + */ + CONFLICT, + + /** + * Concurrency conflict, such as read-modify-write conflict. + */ + ABORTED, + + /** + * The resource that a client tried to create already exists. + */ + ALREADY_EXISTS, + + /** + * Either out of resource quota or rate limited. + */ + RESOURCE_EXHAUSTED, + + /** + * Request cancelled by the client. + */ + CANCELLED, + + /** + * Unrecoverable data loss or data corruption. The client should report the error to the user. + */ + DATA_LOSS, + + /** + * Unknown server error. Typically a server bug. + */ + UNKNOWN, + + /** + * Internal server error. Typically a server bug. + */ + INTERNAL, + + /** + * Service unavailable. Typically the server is down. + */ + UNAVAILABLE, + + /** + * Request deadline exceeded. This happens only if the caller sets a deadline that is + * shorter than the method's default deadline (i.e. requested deadline is not enough for the + * server to process the request) and the request did not finish within the deadline. + */ + DEADLINE_EXCEEDED, +} diff --git a/src/main/java/com/google/firebase/FirebaseApp.java b/src/main/java/com/google/firebase/FirebaseApp.java index c60f0ad5e..38482541c 100644 --- a/src/main/java/com/google/firebase/FirebaseApp.java +++ b/src/main/java/com/google/firebase/FirebaseApp.java @@ -34,9 +34,7 @@ import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; -import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; -import com.google.firebase.internal.FirebaseAppStore; import com.google.firebase.internal.FirebaseScheduledExecutor; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.ListenableFuture2ApiFuture; @@ -48,10 +46,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; @@ -121,7 +117,6 @@ private FirebaseApp(String name, FirebaseOptions options, TokenRefresher.Factory /** Returns a list of all FirebaseApps. */ public static List getApps() { - // TODO: reenable persistence. See b/28158809. synchronized (appsLock) { return ImmutableList.copyOf(instances.values()); } @@ -221,21 +216,16 @@ public static FirebaseApp initializeApp(FirebaseOptions options, String name) { static FirebaseApp initializeApp(FirebaseOptions options, String name, TokenRefresher.Factory tokenRefresherFactory) { - FirebaseAppStore appStore = FirebaseAppStore.initialize(); String normalizedName = normalize(name); - final FirebaseApp firebaseApp; synchronized (appsLock) { checkState( !instances.containsKey(normalizedName), "FirebaseApp name " + normalizedName + " already exists!"); - firebaseApp = new FirebaseApp(normalizedName, options, tokenRefresherFactory); + FirebaseApp firebaseApp = new FirebaseApp(normalizedName, options, tokenRefresherFactory); instances.put(normalizedName, firebaseApp); + return firebaseApp; } - - appStore.persistApp(firebaseApp); - - return firebaseApp; } @VisibleForTesting @@ -251,19 +241,13 @@ static void clearInstancesForTest() { } private static List getAllAppNames() { - Set allAppNames = new HashSet<>(); + List allAppNames; synchronized (appsLock) { - for (FirebaseApp app : instances.values()) { - allAppNames.add(app.getName()); - } - FirebaseAppStore appStore = FirebaseAppStore.getInstance(); - if (appStore != null) { - allAppNames.addAll(appStore.getAllPersistedAppNames()); - } + allAppNames = new ArrayList<>(instances.keySet()); } - List sortedNameList = new ArrayList<>(allAppNames); - Collections.sort(sortedNameList); - return sortedNameList; + + Collections.sort(allAppNames); + return ImmutableList.copyOf(allAppNames); } /** Normalizes the app name. */ @@ -359,11 +343,6 @@ public void delete() { synchronized (appsLock) { instances.remove(name); } - - FirebaseAppStore appStore = FirebaseAppStore.getInstance(); - if (appStore != null) { - appStore.removeApp(name); - } } private void checkNotDeleted() { @@ -582,18 +561,17 @@ enum State { private static FirebaseOptions getOptionsFromEnvironment() throws IOException { String defaultConfig = System.getenv(FIREBASE_CONFIG_ENV_VAR); if (Strings.isNullOrEmpty(defaultConfig)) { - return new FirebaseOptions.Builder() + return FirebaseOptions.builder() .setCredentials(APPLICATION_DEFAULT_CREDENTIALS) .build(); } JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - FirebaseOptions.Builder builder = new FirebaseOptions.Builder(); + FirebaseOptions.Builder builder = FirebaseOptions.builder(); JsonParser parser; if (defaultConfig.startsWith("{")) { parser = jsonFactory.createJsonParser(defaultConfig); } else { - FileReader reader; - reader = new FileReader(defaultConfig); + FileReader reader = new FileReader(defaultConfig); parser = jsonFactory.createJsonParser(reader); } parser.parseAndClose(builder); diff --git a/src/main/java/com/google/firebase/FirebaseAppLifecycleListener.java b/src/main/java/com/google/firebase/FirebaseAppLifecycleListener.java index 60118c249..6493edb60 100644 --- a/src/main/java/com/google/firebase/FirebaseAppLifecycleListener.java +++ b/src/main/java/com/google/firebase/FirebaseAppLifecycleListener.java @@ -19,7 +19,7 @@ /** * A listener which gets notified when {@link com.google.firebase.FirebaseApp} gets deleted. */ -// TODO: consider making it public in a future release. +@Deprecated interface FirebaseAppLifecycleListener { /** diff --git a/src/main/java/com/google/firebase/FirebaseException.java b/src/main/java/com/google/firebase/FirebaseException.java index f78b3fb98..a5bb80424 100644 --- a/src/main/java/com/google/firebase/FirebaseException.java +++ b/src/main/java/com/google/firebase/FirebaseException.java @@ -17,24 +17,55 @@ package com.google.firebase; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Strings; import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; -/** Base class for all Firebase exceptions. */ +/** + * Base class for all Firebase exceptions. + */ public class FirebaseException extends Exception { - // TODO(b/27677218): Exceptions should have non-empty messages. - @Deprecated - protected FirebaseException() {} + private final ErrorCode errorCode; + private final IncomingHttpResponse httpResponse; + + public FirebaseException( + @NonNull ErrorCode errorCode, + @NonNull String message, + @Nullable Throwable cause, + @Nullable IncomingHttpResponse httpResponse) { + super(message, cause); + checkArgument(!Strings.isNullOrEmpty(message), "Message must not be null or empty"); + this.errorCode = checkNotNull(errorCode, "ErrorCode must not be null"); + this.httpResponse = httpResponse; + } + + public FirebaseException( + @NonNull ErrorCode errorCode, + @NonNull String message, + @Nullable Throwable cause) { + this(errorCode, message, cause, null); + } - public FirebaseException(@NonNull String detailMessage) { - super(detailMessage); - checkArgument(!Strings.isNullOrEmpty(detailMessage), "Detail message must not be empty"); + /** + * Returns the platform-wide error code associated with this exception. + * + * @return A Firebase error code. + */ + public final ErrorCode getErrorCode() { + return errorCode; } - public FirebaseException(@NonNull String detailMessage, Throwable cause) { - super(detailMessage, cause); - checkArgument(!Strings.isNullOrEmpty(detailMessage), "Detail message must not be empty"); + /** + * Returns the HTTP response that resulted in this exception. If the exception was not caused by + * an HTTP error response, returns null. + * + * @return An HTTP response or null. + */ + @Nullable + public final IncomingHttpResponse getHttpResponse() { + return httpResponse; } } diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java index f0561d5e5..6ee074d6f 100644 --- a/src/main/java/com/google/firebase/FirebaseOptions.java +++ b/src/main/java/com/google/firebase/FirebaseOptions.java @@ -223,6 +223,16 @@ public static Builder builder() { return new Builder(); } + /** + * Creates a new {@code Builder} from the options object. + * + *

      The new builder is not backed by this object's values; that is, changes made to the new + * builder don't change the values of the origin object. + */ + public Builder toBuilder() { + return new Builder(this); + } + /** * Builder for constructing {@link FirebaseOptions}. */ @@ -249,7 +259,12 @@ public static final class Builder { private int connectTimeout; private int readTimeout; - /** Constructs an empty builder. */ + /** + * Constructs an empty builder. + * + * @deprecated Use {@link FirebaseOptions#builder()} instead. + */ + @Deprecated public Builder() {} /** @@ -257,7 +272,10 @@ public Builder() {} * *

      The new builder is not backed by this object's values, that is changes made to the new * builder don't change the values of the origin object. + * + * @deprecated Use {@link FirebaseOptions#toBuilder()} instead. */ + @Deprecated public Builder(FirebaseOptions options) { databaseUrl = options.databaseUrl; storageBucket = options.storageBucket; diff --git a/src/main/java/com/google/firebase/IncomingHttpResponse.java b/src/main/java/com/google/firebase/IncomingHttpResponse.java new file mode 100644 index 000000000..cfeac5e70 --- /dev/null +++ b/src/main/java/com/google/firebase/IncomingHttpResponse.java @@ -0,0 +1,114 @@ +/* + * Copyright 2020 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; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.database.annotations.Nullable; +import java.util.Map; + +/** + * Contains information that describes an HTTP response received by the SDK. + */ +public final class IncomingHttpResponse { + + private final int statusCode; + private final String content; + private final Map headers; + private final OutgoingHttpRequest request; + + /** + * Creates an {@code IncomingHttpResponse} from a successful response and the content read + * from it. The caller is expected to read the content from the response, and handle any errors + * that may occur while reading. + * + * @param response A successful response. + * @param content Content read from the response. + */ + public IncomingHttpResponse(HttpResponse response, @Nullable String content) { + checkNotNull(response, "response must not be null"); + this.statusCode = response.getStatusCode(); + this.content = content; + this.headers = ImmutableMap.copyOf(response.getHeaders()); + this.request = new OutgoingHttpRequest(response.getRequest()); + } + + /** + * Creates an {@code IncomingHttpResponse} from an HTTP error response. + * + * @param e The exception representing the HTTP error response. + * @param request The request that resulted in the error. + */ + public IncomingHttpResponse(HttpResponseException e, HttpRequest request) { + this(e, new OutgoingHttpRequest(request)); + } + + /** + * Creates an {@code IncomingHttpResponse} from an HTTP error response. + * + * @param e The exception representing the HTTP error response. + * @param request The request that resulted in the error. + */ + public IncomingHttpResponse(HttpResponseException e, OutgoingHttpRequest request) { + checkNotNull(e, "exception must not be null"); + this.statusCode = e.getStatusCode(); + this.content = e.getContent(); + this.headers = ImmutableMap.copyOf(e.getHeaders()); + this.request = checkNotNull(request, "request must not be null"); + } + + /** + * Returns the status code of the response. + * + * @return An HTTP status code (e.g. 500). + */ + public int getStatusCode() { + return this.statusCode; + } + + /** + * Returns the content of the response as a string. + * + * @return HTTP content or null. + */ + @Nullable + public String getContent() { + return this.content; + } + + /** + * Returns the headers set on the response. + * + * @return An immutable map of headers (possibly empty). + */ + public Map getHeaders() { + return this.headers; + } + + /** + * Returns the request that resulted in this response. + * + * @return An HTTP request. + */ + public OutgoingHttpRequest getRequest() { + return request; + } +} diff --git a/src/main/java/com/google/firebase/OutgoingHttpRequest.java b/src/main/java/com/google/firebase/OutgoingHttpRequest.java new file mode 100644 index 000000000..44af4bff0 --- /dev/null +++ b/src/main/java/com/google/firebase/OutgoingHttpRequest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020 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; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpRequest; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.Nullable; +import java.util.Map; + +/** + * Contains the information that describe an HTTP request made by the SDK. + */ +public final class OutgoingHttpRequest { + + private final String method; + private final String url; + private final HttpContent content; + private final Map headers; + + /** + * Creates an {@code OutgoingHttpRequest} from the HTTP method and URL. + * + * @param method HTTP method name. + * @param url Target HTTP URL of the request. + */ + public OutgoingHttpRequest(String method, String url) { + checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(url), "url must not be empty"); + this.method = method; + this.url = url; + this.content = null; + this.headers = ImmutableMap.of(); + } + + OutgoingHttpRequest(HttpRequest request) { + checkNotNull(request, "request must not be null"); + this.method = request.getRequestMethod(); + this.url = request.getUrl().toString(); + this.content = request.getContent(); + this.headers = ImmutableMap.copyOf(request.getHeaders()); + } + + /** + * Returns the HTTP method of the request. + * + * @return An HTTP method string (e.g. GET). + */ + public String getMethod() { + return method; + } + + /** + * Returns the URL of the request. + * + * @return An absolute HTTP URL. + */ + public String getUrl() { + return url; + } + + /** + * Returns any content that was sent with the request. + * + * @return HTTP content or null. + */ + @Nullable + public HttpContent getContent() { + return content; + } + + /** + * Returns the headers set on the request. + * + * @return An immutable map of headers (possibly empty). + */ + public Map getHeaders() { + return headers; + } +} diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 7061d2b44..8004548a5 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -37,7 +37,6 @@ import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; -import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -50,8 +49,6 @@ */ public abstract class AbstractFirebaseAuth { - private static final String ERROR_CUSTOM_TOKEN = "ERROR_CUSTOM_TOKEN"; - private final Object lock = new Object(); private final AtomicBoolean destroyed = new AtomicBoolean(false); @@ -173,12 +170,7 @@ private CallableOperation createCustomTokenOp( return new CallableOperation() { @Override public String execute() throws FirebaseAuthException { - try { - return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims); - } catch (IOException e) { - throw new FirebaseAuthException( - ERROR_CUSTOM_TOKEN, "Failed to generate a custom token", e); - } + return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims); } }; } @@ -902,7 +894,7 @@ protected UserImportResult execute() throws FirebaseAuthException { * not guaranteed to correspond to the nth entry in the input parameters list. * *

      A maximum of 100 identifiers may be specified. If more than 100 identifiers are - * supplied, this method throws an {@link IllegalArgumentException}. + * supplied, this method throws an {@code IllegalArgumentException}. * * @param identifiers The identifiers used to indicate which user records should be returned. Must * have 100 or fewer entries. @@ -924,7 +916,7 @@ public GetUsersResult getUsers(@NonNull Collection identifiers) * not guaranteed to correspond to the nth entry in the input parameters list. * *

      A maximum of 100 identifiers may be specified. If more than 100 identifiers are - * supplied, this method throws an {@link IllegalArgumentException}. + * supplied, this method throws an {@code IllegalArgumentException}. * * @param identifiers The identifiers used to indicate which user records should be returned. * Must have 100 or fewer entries. @@ -978,7 +970,7 @@ private boolean isUserFound(UserIdentifier id, Collection userRecord * DeleteUsersResult.getSuccessCount() value. * *

      A maximum of 1000 identifiers may be supplied. If more than 1000 identifiers are - * supplied, this method throws an {@link IllegalArgumentException}. + * supplied, this method throws an {@code IllegalArgumentException}. * *

      This API has a rate limit of 1 QPS. Exceeding the limit may result in a quota exceeded * error. If you want to delete more than 1000 users, we suggest adding a delay to ensure you @@ -987,7 +979,7 @@ private boolean isUserFound(UserIdentifier id, Collection userRecord * @param uids The uids of the users to be deleted. Must have <= 1000 entries. * @return The total number of successful/failed deletions, as well as the array of errors that * correspond to the failed deletions. - * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 1000 * identifiers are specified. * @throws FirebaseAuthException If an error occurs while deleting users. */ @@ -1003,7 +995,7 @@ public DeleteUsersResult deleteUsers(List uids) throws FirebaseAuthExcep * deletions, as well as the array of errors that correspond to the failed deletions. If an * error occurs while deleting the user account, the future throws a * {@link FirebaseAuthException}. - * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 1000 * identifiers are specified. */ public ApiFuture deleteUsersAsync(List uids) { @@ -1831,11 +1823,7 @@ public FirebaseTokenVerifier get() { new Supplier() { @Override public FirebaseUserManager get() { - return FirebaseUserManager - .builder() - .setFirebaseApp(app) - .setTenantId(tenantId) - .build(); + return FirebaseUserManager.createUserManager(app, tenantId); } }); } diff --git a/src/main/java/com/google/firebase/auth/AuthErrorCode.java b/src/main/java/com/google/firebase/auth/AuthErrorCode.java new file mode 100644 index 000000000..bea067eee --- /dev/null +++ b/src/main/java/com/google/firebase/auth/AuthErrorCode.java @@ -0,0 +1,104 @@ +/* + * Copyright 2020 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; + +/** + * Error codes that can be raised by the Firebase Auth APIs. + */ +public enum AuthErrorCode { + + /** + * Failed to retrieve public key certificates required to verify JWTs. + */ + CERTIFICATE_FETCH_FAILED, + + /** + * No IdP configuration found for the given identifier. + */ + CONFIGURATION_NOT_FOUND, + + /** + * A user already exists with the provided email. + */ + EMAIL_ALREADY_EXISTS, + + /** + * The specified ID token is expired. + */ + EXPIRED_ID_TOKEN, + + /** + * The specified session cookie is expired. + */ + EXPIRED_SESSION_COOKIE, + + /** + * The provided dynamic link domain is not configured or authorized for the current project. + */ + INVALID_DYNAMIC_LINK_DOMAIN, + + /** + * The specified ID token is invalid. + */ + INVALID_ID_TOKEN, + + /** + * The specified session cookie is invalid. + */ + INVALID_SESSION_COOKIE, + + /** + * A user already exists with the provided phone number. + */ + PHONE_NUMBER_ALREADY_EXISTS, + + /** + * The specified ID token has been revoked. + */ + REVOKED_ID_TOKEN, + + /** + * The specified session cookie has been revoked. + */ + REVOKED_SESSION_COOKIE, + + /** + * Tenant ID in the JWT does not match. + */ + TENANT_ID_MISMATCH, + + /** + * No tenant found for the given identifier. + */ + TENANT_NOT_FOUND, + + /** + * A user already exists with the provided UID. + */ + UID_ALREADY_EXISTS, + + /** + * The domain of the continue URL is not whitelisted. Whitelist the domain in the Firebase + * console. + */ + UNAUTHORIZED_CONTINUE_URL, + + /** + * No user record found for the given identifier. + */ + USER_NOT_FOUND, +} diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java index 2314a69d2..53c980668 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java @@ -16,17 +16,11 @@ package com.google.firebase.auth; -// TODO: Move it out from firebase-common. Temporary host it their for -// database's integration.http://b/27624510. - -// TODO: Decide if changing this not enforcing an error code. Need to align -// with the decision in http://b/27677218. Also, need to turn this into abstract later. - -import static com.google.common.base.Preconditions.checkArgument; - -import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; /** * Generic exception related to Firebase Authentication. Check the error code and message for more @@ -34,22 +28,24 @@ */ public class FirebaseAuthException extends FirebaseException { - private final String errorCode; + private final AuthErrorCode errorCode; - public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage) { - this(errorCode, detailMessage, null); + public FirebaseAuthException( + @NonNull ErrorCode errorCode, + @NonNull String message, + Throwable cause, + IncomingHttpResponse response, + AuthErrorCode authErrorCode) { + super(errorCode, message, cause, response); + this.errorCode = authErrorCode; } - public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage, - Throwable throwable) { - super(detailMessage, throwable); - checkArgument(!Strings.isNullOrEmpty(errorCode)); - this.errorCode = errorCode; + public FirebaseAuthException(FirebaseException base) { + this(base.getErrorCode(), base.getMessage(), base.getCause(), base.getHttpResponse(), null); } - /** Returns an error code that may provide more information about the error. */ - @NonNull - public String getErrorCode() { + @Nullable + public AuthErrorCode getAuthErrorCode() { return errorCode; } } diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java index 7c8f9f0a5..873dbe7ac 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java @@ -94,6 +94,8 @@ static FirebaseTokenVerifierImpl createIdTokenVerifier( .setJsonFactory(app.getOptions().getJsonFactory()) .setPublicKeysManager(publicKeysManager) .setIdTokenVerifier(idTokenVerifier) + .setInvalidTokenErrorCode(AuthErrorCode.INVALID_ID_TOKEN) + .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_ID_TOKEN) .setTenantId(tenantId) .build(); } @@ -115,6 +117,8 @@ static FirebaseTokenVerifierImpl createSessionCookieVerifier( .setShortName("session cookie") .setMethod("verifySessionCookie()") .setDocUrl("https://firebase.google.com/docs/auth/admin/manage-cookies") + .setInvalidTokenErrorCode(AuthErrorCode.INVALID_SESSION_COOKIE) + .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_SESSION_COOKIE) .setJsonFactory(app.getOptions().getJsonFactory()) .setPublicKeysManager(publicKeysManager) .setIdTokenVerifier(idTokenVerifier) diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java index e1a5a9a19..273ec1532 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java @@ -28,11 +28,13 @@ import com.google.api.client.util.ArrayMap; import com.google.common.base.Joiner; import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; import com.google.firebase.internal.Nullable; import java.io.IOException; import java.math.BigDecimal; import java.security.GeneralSecurityException; import java.security.PublicKey; +import java.util.List; /** * The default implementation of the {@link FirebaseTokenVerifier} interface. Uses the Google API @@ -44,9 +46,6 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier { private static final String RS256 = "RS256"; private static final String FIREBASE_AUDIENCE = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"; - private static final String ERROR_INVALID_CREDENTIAL = "ERROR_INVALID_CREDENTIAL"; - private static final String ERROR_RUNTIME_EXCEPTION = "ERROR_RUNTIME_EXCEPTION"; - static final String TENANT_ID_MISMATCH_ERROR = "tenant-id-mismatch"; private final JsonFactory jsonFactory; private final GooglePublicKeysManager publicKeysManager; @@ -55,6 +54,8 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier { private final String shortName; private final String articledShortName; private final String docUrl; + private final AuthErrorCode invalidTokenErrorCode; + private final AuthErrorCode expiredTokenErrorCode; private final String tenantId; private FirebaseTokenVerifierImpl(Builder builder) { @@ -68,6 +69,8 @@ private FirebaseTokenVerifierImpl(Builder builder) { this.shortName = builder.shortName; this.articledShortName = prefixWithIndefiniteArticle(this.shortName); this.docUrl = builder.docUrl; + this.invalidTokenErrorCode = checkNotNull(builder.invalidTokenErrorCode); + this.expiredTokenErrorCode = checkNotNull(builder.expiredTokenErrorCode); this.tenantId = Strings.nullToEmpty(builder.tenantId); } @@ -143,38 +146,28 @@ private IdToken parse(String token) throws FirebaseAuthException { shortName, docUrl, articledShortName); - throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, detailedError, e); - } - } - - private void checkContents(final IdToken token) throws FirebaseAuthException { - String errorMessage = getErrorIfContentInvalid(token); - if (errorMessage != null) { - String detailedError = String.format("%s %s", errorMessage, getVerifyTokenMessage()); - throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, detailedError); + throw newException(detailedError, invalidTokenErrorCode, e); } } private void checkSignature(IdToken token) throws FirebaseAuthException { - try { - if (!isSignatureValid(token)) { - throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, - String.format( - "Failed to verify the signature of Firebase %s. %s", - shortName, - getVerifyTokenMessage())); - } - } catch (GeneralSecurityException | IOException e) { - throw new FirebaseAuthException( - ERROR_RUNTIME_EXCEPTION, "Error while verifying signature.", e); + if (!isSignatureValid(token)) { + String message = String.format( + "Failed to verify the signature of Firebase %s. %s", + shortName, + getVerifyTokenMessage()); + throw newException(message, invalidTokenErrorCode); } } - private String getErrorIfContentInvalid(final IdToken idToken) { + private void checkContents(final IdToken idToken) throws FirebaseAuthException { final Header header = idToken.getHeader(); final Payload payload = idToken.getPayload(); + final long currentTimeMillis = idTokenVerifier.getClock().currentTimeMillis(); String errorMessage = null; + AuthErrorCode errorCode = invalidTokenErrorCode; + if (header.getKeyId() == null) { errorMessage = getErrorForTokenWithoutKid(header, payload); } else if (!RS256.equals(header.getAlgorithm())) { @@ -209,14 +202,35 @@ private String getErrorIfContentInvalid(final IdToken idToken) { errorMessage = String.format( "Firebase %s has \"sub\" (subject) claim longer than 128 characters.", shortName); - } else if (!verifyTimestamps(idToken)) { + } else if (!idToken.verifyExpirationTime( + currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds())) { errorMessage = String.format( - "Firebase %s has expired or is not yet valid. Get a fresh %s and try again.", + "Firebase %s has expired. Get a fresh %s and try again.", shortName, shortName); + // Also set the expired error code. + errorCode = expiredTokenErrorCode; + } else if (!idToken.verifyIssuedAtTime( + currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds())) { + errorMessage = String.format( + "Firebase %s is not yet valid.", + shortName); + } + + if (errorMessage != null) { + String detailedError = String.format("%s %s", errorMessage, getVerifyTokenMessage()); + throw newException(detailedError, errorCode); } + } - return errorMessage; + private FirebaseAuthException newException(String message, AuthErrorCode errorCode) { + return newException(message, errorCode, null); + } + + private FirebaseAuthException newException( + String message, AuthErrorCode errorCode, Throwable cause) { + return new FirebaseAuthException( + ErrorCode.INVALID_ARGUMENT, message, cause, null, errorCode); } private String getVerifyTokenMessage() { @@ -230,15 +244,44 @@ private String getVerifyTokenMessage() { * Verifies the cryptographic signature on the FirebaseToken. Can block on a web request to fetch * the keys if they have expired. */ - private boolean isSignatureValid(IdToken token) throws GeneralSecurityException, IOException { - for (PublicKey key : publicKeysManager.getPublicKeys()) { - if (token.verifySignature(key)) { + private boolean isSignatureValid(IdToken token) throws FirebaseAuthException { + for (PublicKey key : fetchPublicKeys()) { + if (isSignatureValid(token, key)) { return true; } } + return false; } + private boolean isSignatureValid(IdToken token, PublicKey key) throws FirebaseAuthException { + try { + return token.verifySignature(key); + } catch (GeneralSecurityException e) { + // This doesn't happen under usual circumstances. Seems to only happen if the crypto + // setup of the runtime is incorrect in some way. + throw new FirebaseAuthException( + ErrorCode.UNKNOWN, + String.format("Unexpected error while verifying %s: %s", shortName, e.getMessage()), + e, + null, + invalidTokenErrorCode); + } + } + + private List fetchPublicKeys() throws FirebaseAuthException { + try { + return publicKeysManager.getPublicKeys(); + } catch (GeneralSecurityException | IOException e) { + throw new FirebaseAuthException( + ErrorCode.UNKNOWN, + "Error while fetching public key certificates: " + e.getMessage(), + e, + null, + AuthErrorCode.CERTIFICATE_FETCH_FAILED); + } + } + private String getErrorForTokenWithoutKid(IdToken.Header header, IdToken.Payload payload) { if (isCustomToken(payload)) { return String.format("%s expects %s, but was given a custom token.", @@ -261,11 +304,6 @@ private String getProjectIdMatchMessage() { shortName); } - private boolean verifyTimestamps(IdToken token) { - long currentTimeMillis = idTokenVerifier.getClock().currentTimeMillis(); - return token.verifyTime(currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds()); - } - private boolean isCustomToken(IdToken.Payload payload) { return FIREBASE_AUDIENCE.equals(payload.getAudience()); } @@ -287,12 +325,11 @@ private boolean containsLegacyUidField(IdToken.Payload payload) { private void checkTenantId(final FirebaseToken firebaseToken) throws FirebaseAuthException { String tokenTenantId = Strings.nullToEmpty(firebaseToken.getTenantId()); if (!this.tenantId.equals(tokenTenantId)) { - throw new FirebaseAuthException( - TENANT_ID_MISMATCH_ERROR, - String.format( - "The tenant ID ('%s') of the token did not match the expected value ('%s')", - tokenTenantId, - tenantId)); + String message = String.format( + "The tenant ID ('%s') of the token did not match the expected value ('%s')", + tokenTenantId, + tenantId); + throw newException(message, AuthErrorCode.TENANT_ID_MISMATCH); } } @@ -308,6 +345,8 @@ static final class Builder { private String shortName; private IdTokenVerifier idTokenVerifier; private String docUrl; + private AuthErrorCode invalidTokenErrorCode; + private AuthErrorCode expiredTokenErrorCode; private String tenantId; private Builder() { } @@ -342,6 +381,16 @@ Builder setDocUrl(String docUrl) { return this; } + Builder setInvalidTokenErrorCode(AuthErrorCode invalidTokenErrorCode) { + this.invalidTokenErrorCode = invalidTokenErrorCode; + return this; + } + + Builder setExpiredTokenErrorCode(AuthErrorCode expiredTokenErrorCode) { + this.expiredTokenErrorCode = expiredTokenErrorCode; + return this; + } + Builder setTenantId(@Nullable String tenantId) { this.tenantId = tenantId; return this; diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index b73882277..554d0179a 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -19,7 +19,6 @@ 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.HttpRequestFactory; import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.json.GenericJson; @@ -30,8 +29,10 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.IncomingHttpResponse; import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.auth.internal.BatchDeleteResponse; import com.google.firebase.auth.internal.DownloadAccountResponse; @@ -41,9 +42,9 @@ import com.google.firebase.auth.internal.ListSamlProviderConfigsResponse; import com.google.firebase.auth.internal.UploadAccountResponse; import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; - import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -57,7 +58,7 @@ * @see * Google Identity Toolkit */ -class FirebaseUserManager { +final class FirebaseUserManager { static final int MAX_LIST_PROVIDER_CONFIGS_RESULTS = 100; static final int MAX_GET_ACCOUNTS_BATCH_SIZE = 100; @@ -78,12 +79,12 @@ class FirebaseUserManager { private final AuthHttpClient httpClient; private FirebaseUserManager(Builder builder) { - FirebaseApp app = checkNotNull(builder.app, "FirebaseApp must not be null"); - String projectId = ImplFirebaseTrampolines.getProjectId(app); + String projectId = builder.projectId; 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.jsonFactory = checkNotNull(builder.jsonFactory, "JsonFactory must not be null"); final String idToolkitUrlV1 = String.format(ID_TOOLKIT_URL, "v1", projectId); final String idToolkitUrlV2 = String.format(ID_TOOLKIT_URL, "v2", projectId); final String tenantId = builder.tenantId; @@ -96,10 +97,7 @@ private FirebaseUserManager(Builder builder) { this.idpConfigMgtBaseUrl = idToolkitUrlV2 + "/tenants/" + tenantId; } - this.jsonFactory = app.getOptions().getJsonFactory(); - HttpRequestFactory requestFactory = builder.requestFactory == null - ? ApiClientUtils.newAuthorizedRequestFactory(app) : builder.requestFactory; - this.httpClient = new AuthHttpClient(jsonFactory, requestFactory); + this.httpClient = new AuthHttpClient(jsonFactory, builder.requestFactory); } @VisibleForTesting @@ -110,40 +108,19 @@ void setInterceptor(HttpResponseInterceptor interceptor) { UserRecord getUserById(String uid) throws FirebaseAuthException { final Map payload = ImmutableMap.of( "localId", ImmutableList.of(uid)); - GetAccountInfoResponse response = post( - "/accounts:lookup", payload, GetAccountInfoResponse.class); - if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException( - AuthHttpClient.USER_NOT_FOUND_ERROR, - "No user record found for the provided user ID: " + uid); - } - return new UserRecord(response.getUsers().get(0), jsonFactory); + return lookupUserAccount(payload, "user ID: " + uid); } UserRecord getUserByEmail(String email) throws FirebaseAuthException { final Map payload = ImmutableMap.of( "email", ImmutableList.of(email)); - GetAccountInfoResponse response = post( - "/accounts:lookup", payload, GetAccountInfoResponse.class); - if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException( - AuthHttpClient.USER_NOT_FOUND_ERROR, - "No user record found for the provided email: " + email); - } - return new UserRecord(response.getUsers().get(0), jsonFactory); + return lookupUserAccount(payload, "email: " + email); } UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException { final Map payload = ImmutableMap.of( "phoneNumber", ImmutableList.of(phoneNumber)); - GetAccountInfoResponse response = post( - "/accounts:lookup", payload, GetAccountInfoResponse.class); - if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException( - AuthHttpClient.USER_NOT_FOUND_ERROR, - "No user record found for the provided phone number: " + phoneNumber); - } - return new UserRecord(response.getUsers().get(0), jsonFactory); + return lookupUserAccount(payload, "phone number: " + phoneNumber); } Set getAccountInfo(@NonNull Collection identifiers) @@ -159,12 +136,6 @@ Set getAccountInfo(@NonNull Collection identifiers) GetAccountInfoResponse response = post( "/accounts:lookup", payload, GetAccountInfoResponse.class); - - if (response == null) { - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to parse server response"); - } - Set results = new HashSet<>(); if (response.getUsers() != null) { for (GetAccountInfoResponse.User user : response.getUsers()) { @@ -175,51 +146,26 @@ Set getAccountInfo(@NonNull Collection identifiers) } String createUser(UserRecord.CreateRequest request) throws FirebaseAuthException { - GenericJson response = post( - "/accounts", request.getProperties(), GenericJson.class); - if (response != null) { - String uid = (String) response.get("localId"); - if (!Strings.isNullOrEmpty(uid)) { - return uid; - } - } - throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to create new user"); + GenericJson response = post("/accounts", request.getProperties(), GenericJson.class); + return (String) response.get("localId"); } void updateUser(UserRecord.UpdateRequest request, JsonFactory jsonFactory) throws FirebaseAuthException { - GenericJson response = post( - "/accounts:update", request.getProperties(jsonFactory), GenericJson.class); - if (response == null || !request.getUid().equals(response.get("localId"))) { - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to update user: " + request.getUid()); - } + post("/accounts:update", request.getProperties(jsonFactory), GenericJson.class); } void deleteUser(String uid) throws FirebaseAuthException { final Map payload = ImmutableMap.of("localId", uid); - GenericJson response = post( - "/accounts:delete", payload, GenericJson.class); - if (response == null || !response.containsKey("kind")) { - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to delete user: " + uid); - } + post("/accounts:delete", payload, GenericJson.class); } - /** - * @pre uids != null - * @pre uids.size() <= MAX_DELETE_ACCOUNTS_BATCH_SIZE - */ DeleteUsersResult deleteUsers(@NonNull List uids) throws FirebaseAuthException { final Map payload = ImmutableMap.of( "localIds", uids, "force", true); BatchDeleteResponse response = post( "/accounts:batchDelete", payload, BatchDeleteResponse.class); - if (response == null) { - throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to delete users"); - } - return new DeleteUsersResult(uids.size(), response); } @@ -231,23 +177,16 @@ DownloadAccountResponse listUsers(int maxResults, String pageToken) throws Fireb builder.put("nextPageToken", pageToken); } - GenericUrl url = new GenericUrl(userMgtBaseUrl + "/accounts:batchGet"); - url.putAll(builder.build()); - DownloadAccountResponse response = httpClient.sendRequest( - "GET", url, null, DownloadAccountResponse.class); - if (response == null) { - throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve users."); - } - return response; + String url = userMgtBaseUrl + "/accounts:batchGet"; + HttpRequestInfo requestInfo = HttpRequestInfo.buildGetRequest(url) + .addAllParameters(builder.build()); + return httpClient.sendRequest(requestInfo, DownloadAccountResponse.class); } UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthException { checkNotNull(request); UploadAccountResponse response = post( "/accounts:batchCreate", request, UploadAccountResponse.class); - if (response == null) { - throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to import users."); - } return new UserImportResult(request.getUsersCount(), response); } @@ -256,14 +195,7 @@ String createSessionCookie(String idToken, final Map payload = ImmutableMap.of( "idToken", idToken, "validDuration", options.getExpiresInSeconds()); GenericJson response = post(":createSessionCookie", payload, GenericJson.class); - if (response != null) { - String cookie = (String) response.get("sessionCookie"); - if (!Strings.isNullOrEmpty(cookie)) { - return cookie; - } - } - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to create session cookie"); + return (String) response.get("sessionCookie"); } String getEmailActionLink(EmailLinkType type, String email, @@ -275,57 +207,70 @@ String getEmailActionLink(EmailLinkType type, String email, 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; - } + return (String) response.get("oobLink"); + } + + private UserRecord lookupUserAccount( + Map payload, String identifier) throws FirebaseAuthException { + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPostRequest( + userMgtBaseUrl + "/accounts:lookup", payload); + IncomingHttpResponse response = httpClient.sendRequest(requestInfo); + GetAccountInfoResponse parsed = httpClient.parse(response, GetAccountInfoResponse.class); + if (parsed.getUsers() == null || parsed.getUsers().isEmpty()) { + throw new FirebaseAuthException(ErrorCode.NOT_FOUND, + "No user record found for the provided " + identifier, + null, + response, + AuthErrorCode.USER_NOT_FOUND); } - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to create email action link"); + + return new UserRecord(parsed.getUsers().get(0), jsonFactory); } OidcProviderConfig createOidcProviderConfig( OidcProviderConfig.CreateRequest request) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); - url.set("oauthIdpConfigId", request.getProviderId()); - return httpClient.sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class); + String url = idpConfigMgtBaseUrl + "/oauthIdpConfigs"; + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPostRequest(url, request.getProperties()) + .addParameter("oauthIdpConfigId", request.getProviderId()); + return httpClient.sendRequest(requestInfo, OidcProviderConfig.class); } SamlProviderConfig createSamlProviderConfig( SamlProviderConfig.CreateRequest request) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs"); - url.set("inboundSamlConfigId", request.getProviderId()); - return httpClient.sendRequest("POST", url, request.getProperties(), SamlProviderConfig.class); + String url = idpConfigMgtBaseUrl + "/inboundSamlConfigs"; + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPostRequest(url, request.getProperties()) + .addParameter("inboundSamlConfigId", request.getProviderId()); + return httpClient.sendRequest(requestInfo, SamlProviderConfig.class); } OidcProviderConfig updateOidcProviderConfig(OidcProviderConfig.UpdateRequest request) throws FirebaseAuthException { Map properties = request.getProperties(); - GenericUrl url = - new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(request.getProviderId())); - url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); - return httpClient.sendRequest("PATCH", url, properties, OidcProviderConfig.class); + String url = idpConfigMgtBaseUrl + getOidcUrlSuffix(request.getProviderId()); + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPatchRequest(url, properties) + .addParameter("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest(requestInfo, OidcProviderConfig.class); } SamlProviderConfig updateSamlProviderConfig(SamlProviderConfig.UpdateRequest request) throws FirebaseAuthException { Map properties = request.getProperties(); - GenericUrl url = - new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(request.getProviderId())); - url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); - return httpClient.sendRequest("PATCH", url, properties, SamlProviderConfig.class); + String url = idpConfigMgtBaseUrl + getSamlUrlSuffix(request.getProviderId()); + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPatchRequest(url, properties) + .addParameter("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest(requestInfo, SamlProviderConfig.class); } OidcProviderConfig getOidcProviderConfig(String providerId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); - return httpClient.sendRequest("GET", url, null, OidcProviderConfig.class); + String url = idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId); + return httpClient.sendRequest(HttpRequestInfo.buildGetRequest(url), OidcProviderConfig.class); } SamlProviderConfig getSamlProviderConfig(String providerId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId)); - return httpClient.sendRequest("GET", url, null, SamlProviderConfig.class); + String url = idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId); + return httpClient.sendRequest(HttpRequestInfo.buildGetRequest(url), SamlProviderConfig.class); } ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String pageToken) @@ -338,15 +283,10 @@ ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String p builder.put("nextPageToken", pageToken); } - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); - url.putAll(builder.build()); - ListOidcProviderConfigsResponse response = - httpClient.sendRequest("GET", url, null, ListOidcProviderConfigsResponse.class); - if (response == null) { - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve provider configs."); - } - return response; + String url = idpConfigMgtBaseUrl + "/oauthIdpConfigs"; + HttpRequestInfo requestInfo = HttpRequestInfo.buildGetRequest(url) + .addAllParameters(builder.build()); + return httpClient.sendRequest(requestInfo, ListOidcProviderConfigsResponse.class); } ListSamlProviderConfigsResponse listSamlProviderConfigs(int maxResults, String pageToken) @@ -359,25 +299,20 @@ ListSamlProviderConfigsResponse listSamlProviderConfigs(int maxResults, String p builder.put("nextPageToken", pageToken); } - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs"); - url.putAll(builder.build()); - ListSamlProviderConfigsResponse response = - httpClient.sendRequest("GET", url, null, ListSamlProviderConfigsResponse.class); - if (response == null) { - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve provider configs."); - } - return response; + String url = idpConfigMgtBaseUrl + "/inboundSamlConfigs"; + HttpRequestInfo requestInfo = HttpRequestInfo.buildGetRequest(url) + .addAllParameters(builder.build()); + return httpClient.sendRequest(requestInfo, ListSamlProviderConfigsResponse.class); } void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); - httpClient.sendRequest("DELETE", url, null, GenericJson.class); + String url = idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId); + httpClient.sendRequest(HttpRequestInfo.buildDeleteRequest(url)); } void deleteSamlProviderConfig(String providerId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId)); - httpClient.sendRequest("DELETE", url, null, GenericJson.class); + String url = idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId); + httpClient.sendRequest(HttpRequestInfo.buildDeleteRequest(url)); } private static String getOidcUrlSuffix(String providerId) { @@ -393,8 +328,8 @@ private static String getSamlUrlSuffix(String providerId) { 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"); - GenericUrl url = new GenericUrl(userMgtBaseUrl + path); - return httpClient.sendRequest("POST", url, content, clazz); + String url = userMgtBaseUrl + path; + return httpClient.sendRequest(HttpRequestInfo.buildJsonPostRequest(url, content), clazz); } static class UserImportRequest extends GenericJson { @@ -437,18 +372,30 @@ enum EmailLinkType { PASSWORD_RESET, } + static FirebaseUserManager createUserManager(FirebaseApp app, String tenantId) { + return FirebaseUserManager.builder() + .setProjectId(ImplFirebaseTrampolines.getProjectId(app)) + .setTenantId(tenantId) + .setHttpRequestFactory(ApiClientUtils.newAuthorizedRequestFactory(app)) + .setJsonFactory(app.getOptions().getJsonFactory()) + .build(); + } + static Builder builder() { return new Builder(); } static class Builder { - private FirebaseApp app; + private String projectId; private String tenantId; private HttpRequestFactory requestFactory; + private JsonFactory jsonFactory; - Builder setFirebaseApp(FirebaseApp app) { - this.app = app; + private Builder() { } + + public Builder setProjectId(String projectId) { + this.projectId = projectId; return this; } @@ -462,6 +409,11 @@ Builder setHttpRequestFactory(HttpRequestFactory requestFactory) { return this; } + public Builder setJsonFactory(JsonFactory jsonFactory) { + this.jsonFactory = jsonFactory; + return this; + } + FirebaseUserManager build() { return new FirebaseUserManager(this); } diff --git a/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java index 361f932bd..0e35337a9 100644 --- a/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java +++ b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java @@ -88,7 +88,7 @@ public String getNextPageToken() { @Override public ListProviderConfigsPage getNextPage() { if (hasNextPage()) { - Factory factory = new Factory(source, maxResults, currentBatch.getPageToken()); + Factory factory = new Factory<>(source, maxResults, currentBatch.getPageToken()); try { return factory.create(); } catch (FirebaseAuthException e) { @@ -99,25 +99,25 @@ public ListProviderConfigsPage getNextPage() { } /** - * Returns an {@link Iterable} that facilitates transparently iterating over all the provider + * Returns an {@code Iterable} that facilitates transparently iterating over all the provider * configs in the current Firebase project, starting from this page. * - *

      The {@link Iterator} instances produced by the returned {@link Iterable} never buffers more + *

      The {@code Iterator} instances produced by the returned {@code Iterable} never buffers more * than one page of provider configs at a time. It is safe to abandon the iterators (i.e. break * the loops) at any time. * - * @return a new {@link Iterable} instance. + * @return a new {@code Iterable} instance. */ @NonNull @Override public Iterable iterateAll() { - return new ProviderConfigIterable(this); + return new ProviderConfigIterable<>(this); } /** - * Returns an {@link Iterable} over the provider configs in this page. + * Returns an {@code Iterable} over the provider configs in this page. * - * @return a {@link Iterable} instance. + * @return a {@code Iterable} instance. */ @NonNull @Override @@ -136,7 +136,7 @@ private static class ProviderConfigIterable implements @Override @NonNull public Iterator iterator() { - return new ProviderConfigIterator(startingPage); + return new ProviderConfigIterator<>(startingPage); } /** @@ -230,7 +230,7 @@ public ListSamlProviderConfigsResponse fetch(int maxResults, String pageToken) } /** - * A simple factory class for {@link ProviderConfigsPage} instances. + * A simple factory class for {@link ListProviderConfigsPage} instances. * *

      Performs argument validation before attempting to load any provider config data (which is * expensive, and hence may be performed asynchronously on a separate thread). @@ -261,7 +261,7 @@ static class Factory { ListProviderConfigsPage create() throws FirebaseAuthException { ListProviderConfigsResponse batch = source.fetch(maxResults, pageToken); - return new ListProviderConfigsPage(batch, source, maxResults); + return new ListProviderConfigsPage<>(batch, source, maxResults); } } } diff --git a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java index 879b7e79f..26931788e 100644 --- a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -132,7 +132,7 @@ public static final class UpdateRequest extends AbstractUpdateRequestThe returned object should be passed to - * {@link AbstractFirebaseAuth#updateOidcProviderConfig(CreateRequest)} to save the updated + * {@link AbstractFirebaseAuth#updateOidcProviderConfig(UpdateRequest)} to save the updated * config. * * @param providerId A non-null, non-empty provider ID string. diff --git a/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java b/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java index e53ad25c4..74cda69c9 100644 --- a/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java +++ b/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java @@ -20,30 +20,27 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; /** * A decorator for adding token revocation checks to an existing {@link FirebaseTokenVerifier}. */ class RevocationCheckDecorator implements FirebaseTokenVerifier { - static final String ID_TOKEN_REVOKED_ERROR = "id-token-revoked"; - static final String SESSION_COOKIE_REVOKED_ERROR = "session-cookie-revoked"; - private final FirebaseTokenVerifier tokenVerifier; private final FirebaseUserManager userManager; - private final String errorCode; + private final AuthErrorCode errorCode; private final String shortName; private RevocationCheckDecorator( FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager, - String errorCode, + AuthErrorCode errorCode, String shortName) { this.tokenVerifier = checkNotNull(tokenVerifier); this.userManager = checkNotNull(userManager); - checkArgument(!Strings.isNullOrEmpty(errorCode)); + this.errorCode = checkNotNull(errorCode); checkArgument(!Strings.isNullOrEmpty(shortName)); - this.errorCode = errorCode; this.shortName = shortName; } @@ -55,8 +52,14 @@ private RevocationCheckDecorator( public FirebaseToken verifyToken(String token) throws FirebaseAuthException { FirebaseToken firebaseToken = tokenVerifier.verifyToken(token); if (isRevoked(firebaseToken)) { - throw new FirebaseAuthException(errorCode, "Firebase " + shortName + " revoked"); + throw new FirebaseAuthException( + ErrorCode.INVALID_ARGUMENT, + "Firebase " + shortName + " is revoked.", + null, + null, + errorCode); } + return firebaseToken; } @@ -69,12 +72,12 @@ private boolean isRevoked(FirebaseToken firebaseToken) throws FirebaseAuthExcept static RevocationCheckDecorator decorateIdTokenVerifier( FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) { return new RevocationCheckDecorator( - tokenVerifier, userManager, ID_TOKEN_REVOKED_ERROR, "id token"); + tokenVerifier, userManager, AuthErrorCode.REVOKED_ID_TOKEN, "id token"); } static RevocationCheckDecorator decorateSessionCookieVerifier( FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) { return new RevocationCheckDecorator( - tokenVerifier, userManager, SESSION_COOKIE_REVOKED_ERROR, "session cookie"); + tokenVerifier, userManager, AuthErrorCode.REVOKED_SESSION_COOKIE, "session cookie"); } } diff --git a/src/main/java/com/google/firebase/auth/hash/Bcrypt.java b/src/main/java/com/google/firebase/auth/hash/Bcrypt.java index 2b5f89029..9c55d8d56 100644 --- a/src/main/java/com/google/firebase/auth/hash/Bcrypt.java +++ b/src/main/java/com/google/firebase/auth/hash/Bcrypt.java @@ -24,7 +24,7 @@ * Represents the Bcrypt password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class Bcrypt extends UserImportHash { +public final class Bcrypt extends UserImportHash { private Bcrypt() { super("BCRYPT"); diff --git a/src/main/java/com/google/firebase/auth/hash/HmacMd5.java b/src/main/java/com/google/firebase/auth/hash/HmacMd5.java index b67574358..b2ffdb852 100644 --- a/src/main/java/com/google/firebase/auth/hash/HmacMd5.java +++ b/src/main/java/com/google/firebase/auth/hash/HmacMd5.java @@ -20,7 +20,7 @@ * Represents the HMAC MD5 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class HmacMd5 extends Hmac { +public final class HmacMd5 extends Hmac { private HmacMd5(Builder builder) { super("HMAC_MD5", builder); diff --git a/src/main/java/com/google/firebase/auth/hash/HmacSha1.java b/src/main/java/com/google/firebase/auth/hash/HmacSha1.java index a9ecefd6f..964e5e60d 100644 --- a/src/main/java/com/google/firebase/auth/hash/HmacSha1.java +++ b/src/main/java/com/google/firebase/auth/hash/HmacSha1.java @@ -20,7 +20,7 @@ * Represents the HMAC SHA1 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class HmacSha1 extends Hmac { +public final class HmacSha1 extends Hmac { private HmacSha1(Builder builder) { super("HMAC_SHA1", builder); diff --git a/src/main/java/com/google/firebase/auth/hash/HmacSha256.java b/src/main/java/com/google/firebase/auth/hash/HmacSha256.java index 78f131cff..92917e6f3 100644 --- a/src/main/java/com/google/firebase/auth/hash/HmacSha256.java +++ b/src/main/java/com/google/firebase/auth/hash/HmacSha256.java @@ -20,7 +20,7 @@ * Represents the HMAC SHA256 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class HmacSha256 extends Hmac { +public final class HmacSha256 extends Hmac { private HmacSha256(Builder builder) { super("HMAC_SHA256", builder); diff --git a/src/main/java/com/google/firebase/auth/hash/HmacSha512.java b/src/main/java/com/google/firebase/auth/hash/HmacSha512.java index 21e6a2b25..b5a0e09ec 100644 --- a/src/main/java/com/google/firebase/auth/hash/HmacSha512.java +++ b/src/main/java/com/google/firebase/auth/hash/HmacSha512.java @@ -20,7 +20,7 @@ * Represents the HMAC SHA512 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class HmacSha512 extends Hmac { +public final class HmacSha512 extends Hmac { private HmacSha512(Builder builder) { super("HMAC_SHA512", builder); diff --git a/src/main/java/com/google/firebase/auth/hash/Md5.java b/src/main/java/com/google/firebase/auth/hash/Md5.java index 2abbe55ba..353b07f01 100644 --- a/src/main/java/com/google/firebase/auth/hash/Md5.java +++ b/src/main/java/com/google/firebase/auth/hash/Md5.java @@ -20,7 +20,7 @@ * Represents the MD5 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class Md5 extends RepeatableHash { +public final class Md5 extends RepeatableHash { private Md5(Builder builder) { super("MD5", 0, 8192, builder); diff --git a/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java b/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java index 4c5108e35..6c3ffeff2 100644 --- a/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java +++ b/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java @@ -20,7 +20,7 @@ * Represents the PBKDF2 SHA256 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class Pbkdf2Sha256 extends RepeatableHash { +public final class Pbkdf2Sha256 extends RepeatableHash { private Pbkdf2Sha256(Builder builder) { super("PBKDF2_SHA256", 0, 120000, builder); diff --git a/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java b/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java index 8afe3f4ab..647a365b3 100644 --- a/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java +++ b/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java @@ -20,7 +20,7 @@ * Represents the PBKDF SHA1 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class PbkdfSha1 extends RepeatableHash { +public final class PbkdfSha1 extends RepeatableHash { private PbkdfSha1(Builder builder) { super("PBKDF_SHA1", 0, 120000, builder); diff --git a/src/main/java/com/google/firebase/auth/hash/Sha1.java b/src/main/java/com/google/firebase/auth/hash/Sha1.java index 385f4310c..9de01b0b8 100644 --- a/src/main/java/com/google/firebase/auth/hash/Sha1.java +++ b/src/main/java/com/google/firebase/auth/hash/Sha1.java @@ -20,7 +20,7 @@ * Represents the SHA1 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class Sha1 extends RepeatableHash { +public final class Sha1 extends RepeatableHash { private Sha1(Builder builder) { super("SHA1", 1, 8192, builder); diff --git a/src/main/java/com/google/firebase/auth/hash/Sha256.java b/src/main/java/com/google/firebase/auth/hash/Sha256.java index f65aee19a..d0185195e 100644 --- a/src/main/java/com/google/firebase/auth/hash/Sha256.java +++ b/src/main/java/com/google/firebase/auth/hash/Sha256.java @@ -20,7 +20,7 @@ * Represents the SHA256 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class Sha256 extends RepeatableHash { +public final class Sha256 extends RepeatableHash { private Sha256(Builder builder) { super("SHA256", 1, 8192, builder); diff --git a/src/main/java/com/google/firebase/auth/hash/Sha512.java b/src/main/java/com/google/firebase/auth/hash/Sha512.java index e582520a9..f468abe1c 100644 --- a/src/main/java/com/google/firebase/auth/hash/Sha512.java +++ b/src/main/java/com/google/firebase/auth/hash/Sha512.java @@ -20,7 +20,7 @@ * Represents the SHA512 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class Sha512 extends RepeatableHash { +public final class Sha512 extends RepeatableHash { private Sha512(Builder builder) { super("SHA512", 1, 8192, builder); diff --git a/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java b/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java index 49f7d72f5..139fd114f 100644 --- a/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java +++ b/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java @@ -24,7 +24,7 @@ * Represents the Standard Scrypt password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class StandardScrypt extends UserImportHash { +public final class StandardScrypt extends UserImportHash { private final int derivedKeyLength; private final int blockSize; diff --git a/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java b/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java new file mode 100644 index 000000000..e98911407 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java @@ -0,0 +1,222 @@ +/* + * Copyright 2020 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 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.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseException; +import com.google.firebase.auth.AuthErrorCode; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.internal.AbstractHttpErrorHandler; +import com.google.firebase.internal.Nullable; +import java.io.IOException; +import java.util.Map; + +final class AuthErrorHandler extends AbstractHttpErrorHandler { + + private static final Map ERROR_CODES = + ImmutableMap.builder() + .put( + "CONFIGURATION_NOT_FOUND", + new AuthError( + ErrorCode.NOT_FOUND, + "No IdP configuration found corresponding to the provided identifier", + AuthErrorCode.CONFIGURATION_NOT_FOUND)) + .put( + "DUPLICATE_EMAIL", + new AuthError( + ErrorCode.ALREADY_EXISTS, + "The user with the provided email already exists", + AuthErrorCode.EMAIL_ALREADY_EXISTS)) + .put( + "DUPLICATE_LOCAL_ID", + new AuthError( + ErrorCode.ALREADY_EXISTS, + "The user with the provided uid already exists", + AuthErrorCode.UID_ALREADY_EXISTS)) + .put( + "EMAIL_EXISTS", + new AuthError( + ErrorCode.ALREADY_EXISTS, + "The user with the provided email already exists", + AuthErrorCode.EMAIL_ALREADY_EXISTS)) + .put( + "INVALID_DYNAMIC_LINK_DOMAIN", + new AuthError( + ErrorCode.INVALID_ARGUMENT, + "The provided dynamic link domain is not " + + "configured or authorized for the current project", + AuthErrorCode.INVALID_DYNAMIC_LINK_DOMAIN)) + .put( + "PHONE_NUMBER_EXISTS", + new AuthError( + ErrorCode.ALREADY_EXISTS, + "The user with the provided phone number already exists", + AuthErrorCode.PHONE_NUMBER_ALREADY_EXISTS)) + .put( + "TENANT_NOT_FOUND", + new AuthError( + ErrorCode.NOT_FOUND, + "No tenant found for the given identifier", + AuthErrorCode.TENANT_NOT_FOUND)) + .put( + "UNAUTHORIZED_DOMAIN", + new AuthError( + ErrorCode.INVALID_ARGUMENT, + "The domain of the continue URL is not whitelisted", + AuthErrorCode.UNAUTHORIZED_CONTINUE_URL)) + .put( + "USER_NOT_FOUND", + new AuthError( + ErrorCode.NOT_FOUND, + "No user record found for the given identifier", + AuthErrorCode.USER_NOT_FOUND)) + .build(); + + private final JsonFactory jsonFactory; + + AuthErrorHandler(JsonFactory jsonFactory) { + this.jsonFactory = checkNotNull(jsonFactory); + } + + @Override + protected FirebaseAuthException createException(FirebaseException base) { + String response = getResponse(base); + AuthServiceErrorResponse parsed = safeParse(response); + AuthError errorInfo = ERROR_CODES.get(parsed.getCode()); + if (errorInfo != null) { + return new FirebaseAuthException( + errorInfo.getErrorCode(), + errorInfo.buildMessage(parsed), + base.getCause(), + base.getHttpResponse(), + errorInfo.getAuthErrorCode()); + } + + return new FirebaseAuthException(base); + } + + private String getResponse(FirebaseException base) { + if (base.getHttpResponse() == null) { + return null; + } + + return base.getHttpResponse().getContent(); + } + + private AuthServiceErrorResponse safeParse(String response) { + AuthServiceErrorResponse parsed = new AuthServiceErrorResponse(); + if (!Strings.isNullOrEmpty(response)) { + try { + jsonFactory.createJsonParser(response).parse(parsed); + } catch (IOException ignore) { + // Ignore any error that may occur while parsing the error response. The server + // may have responded with a non-json payload. + } + } + + return parsed; + } + + private static class AuthError { + + private final ErrorCode errorCode; + private final String message; + private final AuthErrorCode authErrorCode; + + AuthError(ErrorCode errorCode, String message, AuthErrorCode authErrorCode) { + this.errorCode = errorCode; + this.message = message; + this.authErrorCode = authErrorCode; + } + + ErrorCode getErrorCode() { + return errorCode; + } + + AuthErrorCode getAuthErrorCode() { + return authErrorCode; + } + + String buildMessage(AuthServiceErrorResponse response) { + StringBuilder builder = new StringBuilder(this.message) + .append(" (").append(response.getCode()).append(")"); + String detail = response.getDetail(); + if (!Strings.isNullOrEmpty(detail)) { + builder.append(": ").append(detail); + } else { + builder.append("."); + } + + return builder.toString(); + } + } + + /** + * JSON data binding for JSON error messages sent by Google identity toolkit service. These + * error messages take the form `{"error": {"message": "CODE: OPTIONAL DETAILS"}}`. + */ + private static class AuthServiceErrorResponse { + + @Key("error") + private GenericJson error; + + @Nullable + public String getCode() { + String message = getMessage(); + if (Strings.isNullOrEmpty(message)) { + return null; + } + + int separator = message.indexOf(':'); + if (separator != -1) { + return message.substring(0, separator); + } + + return message; + } + + @Nullable + public String getDetail() { + String message = getMessage(); + if (Strings.isNullOrEmpty(message)) { + return null; + } + + int separator = message.indexOf(':'); + if (separator != -1) { + return message.substring(separator + 1).trim(); + } + + return null; + } + + private String getMessage() { + if (error == null) { + return null; + } + + return (String) error.get("message"); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java index ad77236d1..e8413f13f 100644 --- a/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java +++ b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java @@ -16,26 +16,15 @@ 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.HttpContent; -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.base.Strings; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; +import com.google.firebase.IncomingHttpResponse; import com.google.firebase.auth.FirebaseAuthException; -import com.google.firebase.internal.Nullable; +import com.google.firebase.internal.ErrorHandlingHttpClient; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.SdkUtils; -import java.io.IOException; import java.util.Map; import java.util.Set; @@ -44,45 +33,17 @@ */ public final class AuthHttpClient { - public static final String CONFIGURATION_NOT_FOUND_ERROR = "configuration-not-found"; - public static final String INTERNAL_ERROR = "internal-error"; - public static final String TENANT_NOT_FOUND_ERROR = "tenant-not-found"; - public static final String USER_NOT_FOUND_ERROR = "user-not-found"; - private static final String CLIENT_VERSION_HEADER = "X-Client-Version"; private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion(); - // Map of server-side error codes to SDK error codes. - // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors - private static final Map ERROR_CODES = ImmutableMap.builder() - .put("CLAIMS_TOO_LARGE", "claims-too-large") - .put("CONFIGURATION_NOT_FOUND", CONFIGURATION_NOT_FOUND_ERROR) - .put("INSUFFICIENT_PERMISSION", "insufficient-permission") - .put("DUPLICATE_EMAIL", "email-already-exists") - .put("DUPLICATE_LOCAL_ID", "uid-already-exists") - .put("EMAIL_EXISTS", "email-already-exists") - .put("INVALID_CLAIMS", "invalid-claims") - .put("INVALID_EMAIL", "invalid-email") - .put("INVALID_PAGE_SELECTION", "invalid-page-token") - .put("INVALID_PHONE_NUMBER", "invalid-phone-number") - .put("PHONE_NUMBER_EXISTS", "phone-number-already-exists") - .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") - .put("TENANT_NOT_FOUND", TENANT_NOT_FOUND_ERROR) - .build(); - + private final ErrorHandlingHttpClient httpClient; private final JsonFactory jsonFactory; - private final HttpRequestFactory requestFactory; - - private HttpResponseInterceptor interceptor; public AuthHttpClient(JsonFactory jsonFactory, HttpRequestFactory requestFactory) { + AuthErrorHandler authErrorHandler = new AuthErrorHandler(jsonFactory); + this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, authErrorHandler); this.jsonFactory = jsonFactory; - this.requestFactory = requestFactory; } public static Set generateMask(Map properties) { @@ -101,60 +62,20 @@ public static Set generateMask(Map properties) { } public void setInterceptor(HttpResponseInterceptor interceptor) { - this.interceptor = interceptor; + this.httpClient.setInterceptor(interceptor); } - public T sendRequest( - String method, GenericUrl url, - @Nullable Object content, Class clazz) throws FirebaseAuthException { + public T sendRequest(HttpRequestInfo request, Class clazz) throws FirebaseAuthException { + IncomingHttpResponse response = this.sendRequest(request); + return this.parse(response, clazz); + } - 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 { - HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null; - HttpRequest request = - requestFactory.buildRequest(method.equals("PATCH") ? "POST" : method, url, httpContent); - request.setParser(new JsonObjectParser(jsonFactory)); - request.getHeaders().set(CLIENT_VERSION_HEADER, CLIENT_VERSION); - if (method.equals("PATCH")) { - request.getHeaders().set("X-HTTP-Method-Override", "PATCH"); - } - request.setResponseInterceptor(interceptor); - response = request.execute(); - return response.parseAs(clazz); - } catch (HttpResponseException e) { - // Server responded with an HTTP error - handleHttpError(e); - return null; - } catch (IOException e) { - // All other IO errors (Connection refused, reset, parse error etc.) - throw new FirebaseAuthException( - INTERNAL_ERROR, "Error while calling the Firebase Auth backend service", e); - } finally { - if (response != null) { - try { - response.disconnect(); - } catch (IOException ignored) { - // Ignored - } - } - } + public IncomingHttpResponse sendRequest(HttpRequestInfo request) throws FirebaseAuthException { + request.addHeader(CLIENT_VERSION_HEADER, CLIENT_VERSION); + return httpClient.send(request); } - private void handleHttpError(HttpResponseException e) throws FirebaseAuthException { - try { - HttpErrorResponse response = jsonFactory.fromString(e.getContent(), HttpErrorResponse.class); - String code = ERROR_CODES.get(response.getErrorCode()); - if (code != null) { - throw new FirebaseAuthException(code, "Firebase Auth service responded with an error", e); - } - } catch (IOException ignored) { - // Ignored - } - String msg = String.format( - "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); - throw new FirebaseAuthException(INTERNAL_ERROR, msg, e); + public T parse(IncomingHttpResponse response, Class clazz) throws FirebaseAuthException { + return httpClient.parse(response, clazz); } } diff --git a/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java b/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java index 3036f9f28..2ff30a20c 100644 --- a/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java +++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java @@ -16,6 +16,7 @@ package com.google.firebase.auth.internal; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.internal.NonNull; import java.io.IOException; @@ -32,10 +33,10 @@ interface CryptoSigner { * * @param payload Data to be signed * @return Signature as a byte array - * @throws IOException If an error occurs during signing + * @throws FirebaseAuthException If an error occurs during signing */ @NonNull - byte[] sign(@NonNull byte[] payload) throws IOException; + byte[] sign(@NonNull byte[] payload) throws FirebaseAuthException; /** * Returns the client email of the service account used to sign payloads. diff --git a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java index 6ea70b880..7bc54afdf 100644 --- a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java +++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java @@ -8,10 +8,8 @@ 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.GenericJson; 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; @@ -21,9 +19,13 @@ 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.FirebaseException; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.internal.FirebaseRequestInitializer; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.internal.AbstractPlatformErrorHandler; +import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.ErrorHandlingHttpClient; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.NonNull; import java.io.IOException; import java.util.Map; @@ -34,7 +36,9 @@ public class CryptoSigners { private static final String METADATA_SERVICE_URL = - "http://metadata/computeMetadata/v1/instance/service-accounts/default/email"; + "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email"; + + private CryptoSigners() { } /** * A {@link CryptoSigner} implementation that uses service account credentials or equivalent @@ -69,48 +73,33 @@ 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; + private final ErrorHandlingHttpClient httpClient; 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; + this.httpClient = new ErrorHandlingHttpClient<>( + requestFactory, + jsonFactory, + new IAMErrorHandler(jsonFactory)); } void setInterceptor(HttpResponseInterceptor interceptor) { - this.interceptor = interceptor; + httpClient.setInterceptor(interceptor); } @Override - public byte[] sign(byte[] payload) throws IOException { - String encodedUrl = String.format(IAM_SIGN_BLOB_URL, serviceAccount); - HttpResponse response = null; + public byte[] sign(byte[] payload) throws FirebaseAuthException { 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 - } - } - } + String encodedUrl = String.format(IAM_SIGN_BLOB_URL, serviceAccount); + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPostRequest(encodedUrl, content); + GenericJson parsed = httpClient.sendAndParse(requestInfo, GenericJson.class); + return BaseEncoding.base64().decode((String) parsed.get("signature")); } @Override @@ -119,9 +108,17 @@ public String getAccount() { } } - public static class SignBlobResponse { - @Key("signature") - private String signature; + private static class IAMErrorHandler + extends AbstractPlatformErrorHandler { + + IAMErrorHandler(JsonFactory jsonFactory) { + super(jsonFactory); + } + + @Override + protected FirebaseAuthException createException(FirebaseException base) { + return new FirebaseAuthException(base); + } } /** @@ -136,14 +133,12 @@ public static CryptoSigner getCryptoSigner(FirebaseApp firebaseApp) throws IOExc return new ServiceAccountCryptoSigner((ServiceAccountCredentials) credentials); } - FirebaseOptions options = firebaseApp.getOptions(); - HttpRequestFactory requestFactory = options.getHttpTransport().createRequestFactory( - new FirebaseRequestInitializer(firebaseApp)); - JsonFactory jsonFactory = options.getJsonFactory(); + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(firebaseApp); + JsonFactory jsonFactory = firebaseApp.getOptions().getJsonFactory(); // If the SDK was initialized with a service account email, use it with the IAM service // to sign bytes. - String serviceAccountId = options.getServiceAccountId(); + String serviceAccountId = firebaseApp.getOptions().getServiceAccountId(); if (!Strings.isNullOrEmpty(serviceAccountId)) { return new IAMCryptoSigner(requestFactory, jsonFactory, serviceAccountId); } @@ -156,15 +151,22 @@ public static CryptoSigner getCryptoSigner(FirebaseApp firebaseApp) throws IOExc // 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)); + serviceAccountId = discoverServiceAccountId(firebaseApp); + return new IAMCryptoSigner(requestFactory, jsonFactory, serviceAccountId); + } + + private static String discoverServiceAccountId(FirebaseApp firebaseApp) throws IOException { + HttpRequestFactory metadataRequestFactory = + ApiClientUtils.newUnauthorizedRequestFactory(firebaseApp); + HttpRequest request = metadataRequestFactory.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); + return StringUtils.newStringUtf8(output).trim(); } finally { - response.disconnect(); + ApiClientUtils.disconnectQuietly(response); } } } 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 778911d46..b5aa1e31a 100644 --- a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java +++ b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java @@ -25,7 +25,9 @@ import com.google.api.client.util.Base64; import com.google.api.client.util.Clock; import com.google.api.client.util.StringUtils; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.internal.Nullable; import java.io.IOException; @@ -44,10 +46,6 @@ public class FirebaseTokenFactory { private final CryptoSigner signer; private final String tenantId; - public FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) { - this(jsonFactory, clock, signer, null); - } - public FirebaseTokenFactory( JsonFactory jsonFactory, Clock clock, CryptoSigner signer, @Nullable String tenantId) { this.jsonFactory = checkNotNull(jsonFactory); @@ -56,12 +54,17 @@ public FirebaseTokenFactory( this.tenantId = tenantId; } - String createSignedCustomAuthTokenForUser(String uid) throws IOException { + @VisibleForTesting + FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) { + this(jsonFactory, clock, signer, null); + } + + String createSignedCustomAuthTokenForUser(String uid) throws FirebaseAuthException { return createSignedCustomAuthTokenForUser(uid, null); } public String createSignedCustomAuthTokenForUser( - String uid, Map developerClaims) throws IOException { + String uid, Map developerClaims) throws FirebaseAuthException { checkArgument(!Strings.isNullOrEmpty(uid), "Uid must be provided."); checkArgument(uid.length() <= 128, "Uid must be shorter than 128 characters."); @@ -88,20 +91,33 @@ public String createSignedCustomAuthTokenForUser( String.format("developerClaims must not contain a reserved key: %s", key)); } } + GenericJson jsonObject = new GenericJson(); 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; + private String signPayload( + JsonWebSignature.Header header, + FirebaseCustomAuthToken.Payload payload) throws FirebaseAuthException { + String content = encodePayload(header, payload); byte[] contentBytes = StringUtils.getBytesUtf8(content); String signature = Base64.encodeBase64URLSafeString(signer.sign(contentBytes)); return content + "." + signature; } + + private String encodePayload( + JsonWebSignature.Header header, FirebaseCustomAuthToken.Payload payload) { + try { + String headerString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)); + String payloadString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload)); + return headerString + "." + payloadString; + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to encode JWT with the given claims: " + e.getMessage(), e); + } + } } diff --git a/src/main/java/com/google/firebase/auth/internal/HttpErrorResponse.java b/src/main/java/com/google/firebase/auth/internal/HttpErrorResponse.java deleted file mode 100644 index d4be4b4a6..000000000 --- a/src/main/java/com/google/firebase/auth/internal/HttpErrorResponse.java +++ /dev/null @@ -1,49 +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.auth.internal; - -import com.google.api.client.util.Key; -import com.google.common.base.Strings; - -/** - * JSON data binding for JSON error messages sent by Google identity toolkit service. - */ -public class HttpErrorResponse { - - @Key("error") - private Error error; - - public String getErrorCode() { - if (error != null) { - if (!Strings.isNullOrEmpty(error.getCode())) { - return error.getCode(); - } - } - return "unknown"; - } - - public static class Error { - - @Key("message") - private String code; - - public String getCode() { - return code; - } - } - -} diff --git a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java index 1278e63d5..5b776a49a 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java @@ -19,7 +19,6 @@ 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.HttpRequestFactory; import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.json.GenericJson; @@ -33,6 +32,7 @@ import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.auth.internal.ListTenantsResponse; import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.HttpRequestInfo; import java.util.Map; final class FirebaseTenantClient { @@ -46,15 +46,19 @@ final class FirebaseTenantClient { private final AuthHttpClient httpClient; FirebaseTenantClient(FirebaseApp app) { - checkNotNull(app, "FirebaseApp must not be null"); - String projectId = ImplFirebaseTrampolines.getProjectId(app); + this( + ImplFirebaseTrampolines.getProjectId(checkNotNull(app)), + app.getOptions().getJsonFactory(), + ApiClientUtils.newAuthorizedRequestFactory(app)); + } + + FirebaseTenantClient( + String projectId, JsonFactory jsonFactory, HttpRequestFactory requestFactory) { 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.tenantMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v2", projectId); - JsonFactory jsonFactory = app.getOptions().getJsonFactory(); - HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); this.httpClient = new AuthHttpClient(jsonFactory, requestFactory); } @@ -63,25 +67,28 @@ void setInterceptor(HttpResponseInterceptor interceptor) { } Tenant getTenant(String tenantId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId)); - return httpClient.sendRequest("GET", url, null, Tenant.class); + String url = tenantMgtBaseUrl + getTenantUrlSuffix(tenantId); + return httpClient.sendRequest(HttpRequestInfo.buildGetRequest(url), Tenant.class); } Tenant createTenant(Tenant.CreateRequest request) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants"); - return httpClient.sendRequest("POST", url, request.getProperties(), Tenant.class); + String url = tenantMgtBaseUrl + "/tenants"; + return httpClient.sendRequest( + HttpRequestInfo.buildJsonPostRequest(url, request.getProperties()), + Tenant.class); } Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException { Map properties = request.getProperties(); - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(request.getTenantId())); - url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); - return httpClient.sendRequest("PATCH", url, properties, Tenant.class); + String url = tenantMgtBaseUrl + getTenantUrlSuffix(request.getTenantId()); + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPatchRequest(url, properties) + .addParameter("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest(requestInfo, Tenant.class); } void deleteTenant(String tenantId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId)); - httpClient.sendRequest("DELETE", url, null, GenericJson.class); + String url = tenantMgtBaseUrl + getTenantUrlSuffix(tenantId); + httpClient.sendRequest(HttpRequestInfo.buildDeleteRequest(url), GenericJson.class); } ListTenantsResponse listTenants(int maxResults, String pageToken) @@ -94,14 +101,9 @@ ListTenantsResponse listTenants(int maxResults, String pageToken) builder.put("pageToken", pageToken); } - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants"); - url.putAll(builder.build()); - ListTenantsResponse response = httpClient.sendRequest( - "GET", url, null, ListTenantsResponse.class); - if (response == null) { - throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve tenants."); - } - return response; + HttpRequestInfo requestInfo = HttpRequestInfo.buildGetRequest(tenantMgtBaseUrl + "/tenants") + .addAllParameters(builder.build()); + return httpClient.sendRequest(requestInfo, ListTenantsResponse.class); } private static String getTenantUrlSuffix(String tenantId) { diff --git a/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java b/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java index c1f393ddb..5f9917bce 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java @@ -96,14 +96,14 @@ public ListTenantsPage getNextPage() { } /** - * Returns an {@link Iterable} that facilitates transparently iterating over all the tenants in + * Returns an {@code Iterable} that facilitates transparently iterating over all the tenants in * the current Firebase project, starting from this page. * - *

      The {@link Iterator} instances produced by the returned {@link Iterable} never buffers more + *

      The {@code Iterator} instances produced by the returned {@code Iterable} never buffers more * than one page of tenants at a time. It is safe to abandon the iterators (i.e. break the loops) * at any time. * - * @return a new {@link Iterable} instance. + * @return a new {@code Iterable} instance. */ @NonNull @Override @@ -112,9 +112,9 @@ public Iterable iterateAll() { } /** - * Returns an {@link Iterable} over the tenants in this page. + * Returns an {@code Iterable} over the tenants in this page. * - * @return a {@link Iterable} instance. + * @return a {@code Iterable} instance. */ @NonNull @Override @@ -137,7 +137,7 @@ public Iterator iterator() { } /** - * An {@link Iterator} that cycles through tenants, one at a time. + * An {@code Iterator} that cycles through tenants, one at a time. * *

      It buffers the last retrieved batch of tenants in memory. The {@code maxResults} parameter * is an upper bound on the batch size. diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java index dcb226d28..a30c0b884 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java @@ -54,8 +54,13 @@ public final class TenantManager { * @hide */ public TenantManager(FirebaseApp firebaseApp) { - this.firebaseApp = firebaseApp; - this.tenantClient = new FirebaseTenantClient(firebaseApp); + this(firebaseApp, new FirebaseTenantClient(firebaseApp)); + } + + @VisibleForTesting + TenantManager(FirebaseApp firebaseApp, FirebaseTenantClient tenantClient) { + this.firebaseApp = checkNotNull(firebaseApp); + this.tenantClient = checkNotNull(tenantClient); this.tenantAwareAuths = new HashMap<>(); } diff --git a/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java b/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java index a1cb79688..9b862ba86 100644 --- a/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java +++ b/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java @@ -112,7 +112,7 @@ private static class TokenChangeListenerWrapper implements CredentialsChangedLis } @Override - public void onChanged(OAuth2Credentials credentials) throws IOException { + public void onChanged(OAuth2Credentials credentials) { // When this event fires, it is guaranteed that credentials.getAccessToken() will return a // valid OAuth2 token. final AccessToken accessToken = credentials.getAccessToken(); diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java index 7ac527778..822654402 100644 --- a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java +++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java @@ -17,29 +17,27 @@ package com.google.firebase.iid; 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.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; import com.google.api.core.ApiFuture; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; -import com.google.common.io.ByteStreams; import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.database.annotations.Nullable; +import com.google.firebase.internal.AbstractHttpErrorHandler; +import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.CallableOperation; -import com.google.firebase.internal.FirebaseRequestInitializer; +import com.google.firebase.internal.ErrorHandlingHttpClient; import com.google.firebase.internal.FirebaseService; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.NonNull; -import java.io.IOException; import java.util.Map; /** @@ -64,22 +62,30 @@ public class FirebaseInstanceId { .build(); private final FirebaseApp app; - private final HttpRequestFactory requestFactory; - private final JsonFactory jsonFactory; private final String projectId; - - private HttpResponseInterceptor interceptor; + private final ErrorHandlingHttpClient httpClient; private FirebaseInstanceId(FirebaseApp app) { - HttpTransport httpTransport = app.getOptions().getHttpTransport(); - this.app = app; - this.requestFactory = httpTransport.createRequestFactory(new FirebaseRequestInitializer(app)); - this.jsonFactory = app.getOptions().getJsonFactory(); - this.projectId = ImplFirebaseTrampolines.getProjectId(app); + this(app, null); + } + + @VisibleForTesting + FirebaseInstanceId(FirebaseApp app, @Nullable HttpRequestFactory requestFactory) { + this.app = checkNotNull(app, "app must not be null"); + String projectId = ImplFirebaseTrampolines.getProjectId(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 GOOGLE_CLOUD_PROJECT environment variable."); + this.projectId = projectId; + if (requestFactory == null) { + requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + } + + this.httpClient = new ErrorHandlingHttpClient<>( + requestFactory, + app.getOptions().getJsonFactory(), + new InstanceIdErrorHandler()); } /** @@ -107,7 +113,7 @@ public static synchronized FirebaseInstanceId getInstance(FirebaseApp app) { @VisibleForTesting void setInterceptor(HttpResponseInterceptor interceptor) { - this.interceptor = interceptor; + httpClient.setInterceptor(interceptor); } /** @@ -146,42 +152,45 @@ private CallableOperation deleteInstanceIdOp( protected Void execute() throws FirebaseInstanceIdException { String url = String.format( "%s/project/%s/instanceId/%s", IID_SERVICE_URL, projectId, instanceId); - HttpResponse response = null; - try { - HttpRequest request = requestFactory.buildDeleteRequest(new GenericUrl(url)); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(interceptor); - response = request.execute(); - ByteStreams.exhaust(response.getContent()); - } catch (Exception e) { - handleError(instanceId, e); - } finally { - disconnectQuietly(response); - } + HttpRequestInfo request = HttpRequestInfo.buildDeleteRequest(url); + httpClient.send(request); return null; } }; } - private static void disconnectQuietly(HttpResponse response) { - if (response != null) { - try { - response.disconnect(); - } catch (IOException ignored) { - // ignored + private static class InstanceIdErrorHandler + extends AbstractHttpErrorHandler { + + @Override + protected FirebaseInstanceIdException createException(FirebaseException base) { + String message = base.getMessage(); + String customMessage = getCustomMessage(base); + if (!Strings.isNullOrEmpty(customMessage)) { + message = customMessage; } + + return new FirebaseInstanceIdException(base, message); } - } - private void handleError(String instanceId, Exception e) throws FirebaseInstanceIdException { - String msg = "Error while invoking instance ID service."; - if (e instanceof HttpResponseException) { - int statusCode = ((HttpResponseException) e).getStatusCode(); - if (ERROR_CODES.containsKey(statusCode)) { - msg = String.format("Instance ID \"%s\": %s", instanceId, ERROR_CODES.get(statusCode)); + private String getCustomMessage(FirebaseException base) { + IncomingHttpResponse response = base.getHttpResponse(); + if (response != null) { + String instanceId = extractInstanceId(response); + String description = ERROR_CODES.get(response.getStatusCode()); + if (description != null) { + return String.format("Instance ID \"%s\": %s", instanceId, description); + } } + + return null; + } + + private String extractInstanceId(IncomingHttpResponse response) { + String url = response.getRequest().getUrl(); + int index = url.lastIndexOf('/'); + return url.substring(index + 1); } - throw new FirebaseInstanceIdException(msg, e); } private static final String SERVICE_ID = FirebaseInstanceId.class.getName(); diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java index dfe0087fc..482a23a3d 100644 --- a/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java +++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java @@ -21,9 +21,9 @@ /** * Represents an exception encountered while interacting with the Firebase instance ID service. */ -public class FirebaseInstanceIdException extends FirebaseException { +public final class FirebaseInstanceIdException extends FirebaseException { - FirebaseInstanceIdException(String detailMessage, Throwable cause) { - super(detailMessage, cause); + FirebaseInstanceIdException(FirebaseException base, String message) { + super(base.getErrorCode(), message, base.getCause(), base.getHttpResponse()); } } diff --git a/src/main/java/com/google/firebase/internal/AbstractHttpErrorHandler.java b/src/main/java/com/google/firebase/internal/AbstractHttpErrorHandler.java new file mode 100644 index 000000000..4d1f6b26e --- /dev/null +++ b/src/main/java/com/google/firebase/internal/AbstractHttpErrorHandler.java @@ -0,0 +1,154 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpStatusCodes; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import java.io.IOException; +import java.net.NoRouteToHostException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * An abstract HttpErrorHandler implementation that maps HTTP status codes to Firebase error codes. + * Also provides reasonable default implementations to other error handler methods in the + * HttpErrorHandler interface. + */ +public abstract class AbstractHttpErrorHandler + implements HttpErrorHandler { + + private static final Map HTTP_ERROR_CODES = + ImmutableMap.builder() + .put(HttpStatusCodes.STATUS_CODE_BAD_REQUEST, ErrorCode.INVALID_ARGUMENT) + .put(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED, ErrorCode.UNAUTHENTICATED) + .put(HttpStatusCodes.STATUS_CODE_FORBIDDEN, ErrorCode.PERMISSION_DENIED) + .put(HttpStatusCodes.STATUS_CODE_NOT_FOUND, ErrorCode.NOT_FOUND) + .put(HttpStatusCodes.STATUS_CODE_CONFLICT, ErrorCode.CONFLICT) + .put(429, ErrorCode.RESOURCE_EXHAUSTED) + .put(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, ErrorCode.INTERNAL) + .put(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE, ErrorCode.UNAVAILABLE) + .build(); + + @Override + public final T handleHttpResponseException( + HttpResponseException e, IncomingHttpResponse response) { + FirebaseException base = this.httpResponseErrorToBaseException(e, response); + return this.createException(base); + } + + @Override + public final T handleIOException(IOException e) { + FirebaseException base = this.ioErrorToBaseException(e); + return this.createException(base); + } + + @Override + public final T handleParseException(IOException e, IncomingHttpResponse response) { + FirebaseException base = this.parseErrorToBaseException(e, response); + return this.createException(base); + } + + /** + * Creates a FirebaseException from the given HTTP response error. Error code is determined from + * the HTTP status code of the response. Error message includes both the status code and full + * response payload to aid in debugging. + * + * @param e HTTP response exception. + * @param response Incoming HTTP response. + * @return A FirebaseException instance. + */ + protected FirebaseException httpResponseErrorToBaseException( + HttpResponseException e, IncomingHttpResponse response) { + ErrorCode code = HTTP_ERROR_CODES.get(e.getStatusCode()); + if (code == null) { + code = ErrorCode.UNKNOWN; + } + + String message = String.format("Unexpected HTTP response with status: %d\n%s", + e.getStatusCode(), e.getContent()); + return new FirebaseException(code, message, e, response); + } + + /** + * Creates a FirebaseException from the given IOException. If IOException resulted from a socket + * timeout, sets the error code DEADLINE_EXCEEDED. If the IOException resulted from a network + * outage or other connectivity issue, sets the error code to UNAVAILABLE. In all other cases sets + * the error code to UNKNOWN. + * + * @param e IOException to create the new exception from. + * @return A FirebaseException instance. + */ + protected FirebaseException ioErrorToBaseException(IOException e) { + ErrorCode code = ErrorCode.UNKNOWN; + String message = "Unknown error while making a remote service call" ; + if (isInstance(e, SocketTimeoutException.class)) { + code = ErrorCode.DEADLINE_EXCEEDED; + message = "Timed out while making an API call"; + } + + if (isInstance(e, UnknownHostException.class) || isInstance(e, NoRouteToHostException.class)) { + code = ErrorCode.UNAVAILABLE; + message = "Failed to establish a connection"; + } + + return new FirebaseException(code, message + ": " + e.getMessage(), e); + } + + protected FirebaseException parseErrorToBaseException( + IOException e, IncomingHttpResponse response) { + return new FirebaseException( + ErrorCode.UNKNOWN, "Error while parsing HTTP response: " + e.getMessage(), e, response); + } + + /** + * Converts the given base FirebaseException to a more specific exception type. The base exception + * is guaranteed to have an error code, a message and a cause. But the HTTP response is only set + * if the exception occurred after receiving a response from a remote server. + * + * @param base A FirebaseException. + * @return A more specific exception created from the base. + */ + protected abstract T createException(FirebaseException base); + + /** + * Checks if the given exception stack t contains an instance of type. + */ + private boolean isInstance(IOException t, Class type) { + Throwable current = t; + Set chain = new HashSet<>(); + while (current != null) { + if (!chain.add(current)) { + break; + } + + if (type.isInstance(current)) { + return true; + } + + current = current.getCause(); + } + + return false; + } +} diff --git a/src/main/java/com/google/firebase/internal/AbstractPlatformErrorHandler.java b/src/main/java/com/google/firebase/internal/AbstractPlatformErrorHandler.java new file mode 100644 index 000000000..b0909e4f7 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/AbstractPlatformErrorHandler.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import java.io.IOException; + +/** + * An abstract HttpErrorHandler that handles Google Cloud error responses. Format of these + * error responses are defined at https://cloud.google.com/apis/design/errors. + */ +public abstract class AbstractPlatformErrorHandler + extends AbstractHttpErrorHandler { + + protected final JsonFactory jsonFactory; + + public AbstractPlatformErrorHandler(JsonFactory jsonFactory) { + this.jsonFactory = checkNotNull(jsonFactory, "jsonFactory must not be null"); + } + + @Override + protected final FirebaseException httpResponseErrorToBaseException( + HttpResponseException e, IncomingHttpResponse response) { + FirebaseException base = super.httpResponseErrorToBaseException(e, response); + PlatformErrorResponse parsedError = this.parseErrorResponse(e.getContent()); + + ErrorCode code = base.getErrorCode(); + String status = parsedError.getStatus(); + if (!Strings.isNullOrEmpty(status)) { + code = Enum.valueOf(ErrorCode.class, parsedError.getStatus()); + } + + String message = parsedError.getMessage(); + if (Strings.isNullOrEmpty(message)) { + message = base.getMessage(); + } + + return new FirebaseException(code, message, e, response); + } + + private PlatformErrorResponse parseErrorResponse(String content) { + PlatformErrorResponse response = new PlatformErrorResponse(); + if (!Strings.isNullOrEmpty(content)) { + try { + jsonFactory.createJsonParser(content).parseAndClose(response); + } catch (IOException e) { + // Ignore any error that may occur while parsing the error response. The server + // may have responded with a non-json payload. Return an empty return value, and + // let the base class logic come into play. + } + } + + return response; + } + + public static class PlatformErrorResponse { + @Key("error") + private PlatformError error; + + String getStatus() { + return error != null ? error.status : null; + } + + String getMessage() { + return error != null ? error.message : null; + } + } + + public static class PlatformError { + @Key("status") + private String status; + + @Key("message") + private String message; + } +} diff --git a/src/main/java/com/google/firebase/internal/ApiClientUtils.java b/src/main/java/com/google/firebase/internal/ApiClientUtils.java index 36ccf5cc8..f2724196b 100644 --- a/src/main/java/com/google/firebase/internal/ApiClientUtils.java +++ b/src/main/java/com/google/firebase/internal/ApiClientUtils.java @@ -35,6 +35,8 @@ public class ApiClientUtils { .setMaxIntervalMillis(60 * 1000) .build(); + private ApiClientUtils() { } + /** * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and * automatic retries. diff --git a/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java b/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java new file mode 100644 index 000000000..5efdd0ec2 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java @@ -0,0 +1,144 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +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.json.JsonFactory; +import com.google.api.client.json.JsonParser; +import com.google.common.io.CharStreams; +import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * An HTTP client implementation that handles any errors that may occur during HTTP calls, and + * converts them into an instance of FirebaseException. + */ +public final class ErrorHandlingHttpClient { + + private final HttpRequestFactory requestFactory; + private final JsonFactory jsonFactory; + private final HttpErrorHandler errorHandler; + + private HttpResponseInterceptor interceptor; + + public ErrorHandlingHttpClient( + HttpRequestFactory requestFactory, + JsonFactory jsonFactory, + HttpErrorHandler errorHandler) { + this.requestFactory = checkNotNull(requestFactory, "requestFactory must not be null"); + this.jsonFactory = checkNotNull(jsonFactory, "jsonFactory must not be null"); + this.errorHandler = checkNotNull(errorHandler, "errorHandler must not be null"); + } + + public ErrorHandlingHttpClient setInterceptor(HttpResponseInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + /** + * Sends the given HTTP request to the target endpoint, and parses the response while handling + * any errors that may occur along the way. + * + * @param requestInfo Outgoing request configuration. + * @param responseType Class to parse the response into. + * @param Parsed response type. + * @return Parsed response object. + * @throws T If any error occurs while making the request. + */ + public V sendAndParse(HttpRequestInfo requestInfo, Class responseType) throws T { + IncomingHttpResponse response = send(requestInfo); + return parse(response, responseType); + } + + /** + * Sends the given HTTP request to the target endpoint, and parses the response while handling + * any errors that may occur along the way. This method can be used when the response should + * be parsed into an instance of a private or protected class, which cannot be instantiated + * outside the call-site. + * + * @param requestInfo Outgoing request configuration. + * @param destination Object to parse the response into. + * @throws T If any error occurs while making the request. + */ + public void sendAndParse(HttpRequestInfo requestInfo, Object destination) throws T { + IncomingHttpResponse response = send(requestInfo); + parse(response, destination); + } + + public IncomingHttpResponse send(HttpRequestInfo requestInfo) throws T { + HttpRequest request = createHttpRequest(requestInfo); + + HttpResponse response = null; + try { + response = request.execute(); + // Read and buffer the content. Otherwise if a parse error occurs later, + // we lose the content stream. + String content = null; + InputStream stream = response.getContent(); + if (stream != null) { + // Stream is null when the response body is empty (e.g. 204 No Content responses). + content = CharStreams.toString(new InputStreamReader(stream, response.getContentCharset())); + } + + return new IncomingHttpResponse(response, content); + } catch (HttpResponseException e) { + throw errorHandler.handleHttpResponseException(e, new IncomingHttpResponse(e, request)); + } catch (IOException e) { + throw errorHandler.handleIOException(e); + } finally { + ApiClientUtils.disconnectQuietly(response); + } + } + + public V parse(IncomingHttpResponse response, Class responseType) throws T { + checkNotNull(responseType, "responseType must not be null"); + try { + JsonParser parser = jsonFactory.createJsonParser(response.getContent()); + return parser.parseAndClose(responseType); + } catch (IOException e) { + throw errorHandler.handleParseException(e, response); + } + } + + public void parse(IncomingHttpResponse response, Object destination) throws T { + try { + JsonParser parser = jsonFactory.createJsonParser(response.getContent()); + parser.parse(destination); + } catch (IOException e) { + throw errorHandler.handleParseException(e, response); + } + } + + private HttpRequest createHttpRequest(HttpRequestInfo requestInfo) throws T { + try { + return requestInfo.newHttpRequest(requestFactory, jsonFactory) + .setResponseInterceptor(interceptor); + } catch (IOException e) { + // Handle request initialization errors (credential loading and other config errors) + throw errorHandler.handleIOException(e); + } + } +} diff --git a/src/main/java/com/google/firebase/internal/FirebaseAppStore.java b/src/main/java/com/google/firebase/internal/FirebaseAppStore.java deleted file mode 100644 index 778295655..000000000 --- a/src/main/java/com/google/firebase/internal/FirebaseAppStore.java +++ /dev/null @@ -1,78 +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.internal; - -import com.google.common.annotations.VisibleForTesting; -import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseOptions; - -import java.util.Collections; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; - -/** No-op base class of FirebaseAppStore. */ -public class FirebaseAppStore { - - private static final AtomicReference sInstance = new AtomicReference<>(); - - FirebaseAppStore() {} - - @Nullable - public static FirebaseAppStore getInstance() { - return sInstance.get(); - } - - // TODO: reenable persistence. See b/28158809. - public static FirebaseAppStore initialize() { - sInstance.compareAndSet(null /* expected */, new FirebaseAppStore()); - return sInstance.get(); - } - - /** - * @hide - */ - public static void setInstanceForTest(FirebaseAppStore firebaseAppStore) { - sInstance.set(firebaseAppStore); - } - - @VisibleForTesting - public static void clearInstanceForTest() { - FirebaseAppStore instance = sInstance.get(); - if (instance != null) { - instance.resetStore(); - } - sInstance.set(null); - } - - /** The returned set is mutable. */ - public Set getAllPersistedAppNames() { - return Collections.emptySet(); - } - - public void persistApp(@NonNull FirebaseApp app) {} - - public void removeApp(@NonNull String name) {} - - /** - * @return The restored {@link FirebaseOptions}, or null if it doesn't exist. - */ - public FirebaseOptions restoreAppOptions(@NonNull String name) { - return null; - } - - protected void resetStore() {} -} diff --git a/src/main/java/com/google/firebase/internal/HttpErrorHandler.java b/src/main/java/com/google/firebase/internal/HttpErrorHandler.java new file mode 100644 index 000000000..988b45a45 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/HttpErrorHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import com.google.api.client.http.HttpResponseException; +import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import java.io.IOException; + +/** + * An interface for handling all sorts of exceptions that may occur while making an HTTP call and + * converting them into some instance of FirebaseException. + */ +public interface HttpErrorHandler { + + /** + * Handle any low-level transport and initialization errors. + */ + T handleIOException(IOException e); + + /** + * Handle HTTP response exceptions (caused by HTTP error responses). + */ + T handleHttpResponseException(HttpResponseException e, IncomingHttpResponse response); + + /** + * Handle any errors that may occur while parsing the response payload. + */ + T handleParseException(IOException e, IncomingHttpResponse response); +} diff --git a/src/main/java/com/google/firebase/internal/HttpRequestInfo.java b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java new file mode 100644 index 000000000..375e332fb --- /dev/null +++ b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java @@ -0,0 +1,132 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static 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.HttpContent; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.JsonFactory; +import com.google.common.base.Strings; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Internal API for configuring outgoing HTTP requests. To be used with the + * {@link ErrorHandlingHttpClient} class. + */ +public final class HttpRequestInfo { + + private final String method; + private final GenericUrl url; + private final HttpContent content; + private final Object jsonContent; + private final Map headers = new HashMap<>(); + + private HttpRequestInfo(String method, GenericUrl url, HttpContent content, Object jsonContent) { + checkArgument(!Strings.isNullOrEmpty(method), "method must not be null"); + this.method = method; + this.url = checkNotNull(url, "url must not be null"); + this.content = content; + this.jsonContent = jsonContent; + } + + public HttpRequestInfo addHeader(String name, String value) { + this.headers.put(name, value); + return this; + } + + public HttpRequestInfo addAllHeaders(Map headers) { + this.headers.putAll(headers); + return this; + } + + public HttpRequestInfo addParameter(String name, Object value) { + this.url.put(name, value); + return this; + } + + public HttpRequestInfo addAllParameters(Map params) { + this.url.putAll(params); + return this; + } + + public static HttpRequestInfo buildGetRequest(String url) { + return buildRequest(HttpMethods.GET, url, null); + } + + public static HttpRequestInfo buildDeleteRequest(String url) { + return buildRequest(HttpMethods.DELETE, url, null); + } + + public static HttpRequestInfo buildRequest( + String method, String url, @Nullable HttpContent content) { + return new HttpRequestInfo(method, new GenericUrl(url), content, null); + } + + public static HttpRequestInfo buildJsonPostRequest(String url, @Nullable Object content) { + return buildJsonRequest(HttpMethods.POST, url, content); + } + + public static HttpRequestInfo buildJsonPatchRequest(String url, @Nullable Object content) { + return buildJsonRequest(HttpMethods.PATCH, url, content); + } + + public static HttpRequestInfo buildJsonRequest( + String method, String url, @Nullable Object content) { + return new HttpRequestInfo(method, new GenericUrl(url), null, content); + } + + HttpRequest newHttpRequest( + HttpRequestFactory factory, JsonFactory jsonFactory) throws IOException { + HttpRequest request; + HttpContent httpContent = getContent(jsonFactory); + if (factory.getTransport().supportsMethod(method)) { + request = factory.buildRequest(method, url, httpContent); + } else { + // Some HttpTransport implementations (notably NetHttpTransport) don't support new methods + // like PATCH. We try to emulate such requests over POST by setting the method override + // header, which is recognized by most Google backend APIs. + request = factory.buildPostRequest(url, httpContent); + request.getHeaders().set("X-HTTP-Method-Override", method); + } + + for (Map.Entry entry : headers.entrySet()) { + request.getHeaders().set(entry.getKey(), entry.getValue()); + } + + return request; + } + + private HttpContent getContent(JsonFactory jsonFactory) { + if (content != null) { + return content; + } + + if (jsonContent != null) { + return new JsonHttpContent(jsonFactory, jsonContent); + } + + return null; + } +} diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index ee25957b3..e1b4f794c 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -41,10 +41,6 @@ */ public class FirebaseMessaging { - static final String INTERNAL_ERROR = "internal-error"; - - static final String UNKNOWN_ERROR = "unknown-error"; - private final FirebaseApp app; private final Supplier messagingClient; private final Supplier instanceIdClient; diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java index 53d5ab00b..43b9b340b 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java @@ -21,26 +21,33 @@ import com.google.api.client.googleapis.batch.BatchCallback; import com.google.api.client.googleapis.batch.BatchRequest; +import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClient; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpRequestInitializer; -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.HttpTransport; 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.json.JsonParser; 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.ErrorCode; import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.OutgoingHttpRequest; +import com.google.firebase.internal.AbstractPlatformErrorHandler; import com.google.firebase.internal.ApiClientUtils; -import com.google.firebase.internal.Nullable; +import com.google.firebase.internal.ErrorHandlingHttpClient; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.SdkUtils; import com.google.firebase.messaging.internal.MessagingServiceErrorResponse; import com.google.firebase.messaging.internal.MessagingServiceResponse; @@ -55,37 +62,19 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient { private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; - private static final String FCM_BATCH_URL = "https://fcm.googleapis.com/batch"; - - private static final String API_FORMAT_VERSION_HEADER = "X-GOOG-API-FORMAT-VERSION"; - - private static final String CLIENT_VERSION_HEADER = "X-Firebase-Client"; - - private static final Map FCM_ERROR_CODES = - ImmutableMap.builder() - // FCM v1 canonical error codes - .put("NOT_FOUND", "registration-token-not-registered") - .put("PERMISSION_DENIED", "mismatched-credential") - .put("RESOURCE_EXHAUSTED", "message-rate-exceeded") - .put("UNAUTHENTICATED", "third-party-auth-error") - - // FCM v1 new error codes - .put("APNS_AUTH_ERROR", "third-party-auth-error") - .put("INTERNAL", FirebaseMessaging.INTERNAL_ERROR) - .put("INVALID_ARGUMENT", "invalid-argument") - .put("QUOTA_EXCEEDED", "message-rate-exceeded") - .put("SENDER_ID_MISMATCH", "mismatched-credential") - .put("THIRD_PARTY_AUTH_ERROR", "third-party-auth-error") - .put("UNAVAILABLE", "server-unavailable") - .put("UNREGISTERED", "registration-token-not-registered") - .build(); + private static final Map COMMON_HEADERS = + ImmutableMap.of( + "X-GOOG-API-FORMAT-VERSION", "2", + "X-Firebase-Client", "fire-admin-java/" + SdkUtils.getVersion()); private final String fcmSendUrl; private final HttpRequestFactory requestFactory; private final HttpRequestFactory childRequestFactory; private final JsonFactory jsonFactory; private final HttpResponseInterceptor responseInterceptor; - private final String clientVersion = "fire-admin-java/" + SdkUtils.getVersion(); + private final MessagingErrorHandler errorHandler; + private final ErrorHandlingHttpClient httpClient; + private final MessagingBatchClient batchClient; private FirebaseMessagingClientImpl(Builder builder) { checkArgument(!Strings.isNullOrEmpty(builder.projectId)); @@ -94,6 +83,10 @@ private FirebaseMessagingClientImpl(Builder builder) { this.childRequestFactory = checkNotNull(builder.childRequestFactory); this.jsonFactory = checkNotNull(builder.jsonFactory); this.responseInterceptor = builder.responseInterceptor; + this.errorHandler = new MessagingErrorHandler(this.jsonFactory); + this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler) + .setInterceptor(responseInterceptor); + this.batchClient = new MessagingBatchClient(requestFactory.getTransport(), jsonFactory); } @VisibleForTesting @@ -116,67 +109,48 @@ JsonFactory getJsonFactory() { return jsonFactory; } - @VisibleForTesting - String getClientVersion() { - return clientVersion; - } - public String send(Message message, boolean dryRun) throws FirebaseMessagingException { - try { - return sendSingleRequest(message, dryRun); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); - } + return sendSingleRequest(message, dryRun); } public BatchResponse sendAll( List messages, boolean dryRun) throws FirebaseMessagingException { - try { - return sendBatchRequest(messages, dryRun); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); - } + return sendBatchRequest(messages, dryRun); } - private String sendSingleRequest(Message message, boolean dryRun) throws IOException { - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(fcmSendUrl), - new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); - setCommonFcmHeaders(request.getHeaders()); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(responseInterceptor); - HttpResponse response = request.execute(); - try { - MessagingServiceResponse parsed = new MessagingServiceResponse(); - jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); - return parsed.getMessageId(); - } finally { - ApiClientUtils.disconnectQuietly(response); - } + private String sendSingleRequest( + Message message, boolean dryRun) throws FirebaseMessagingException { + HttpRequestInfo request = + HttpRequestInfo.buildJsonPostRequest( + fcmSendUrl, message.wrapForTransport(dryRun)) + .addAllHeaders(COMMON_HEADERS); + MessagingServiceResponse parsed = httpClient.sendAndParse( + request, MessagingServiceResponse.class); + return parsed.getMessageId(); } private BatchResponse sendBatchRequest( - List messages, boolean dryRun) throws IOException { + List messages, boolean dryRun) throws FirebaseMessagingException { MessagingBatchCallback callback = new MessagingBatchCallback(); - BatchRequest batch = newBatchRequest(messages, dryRun, callback); - batch.execute(); - return new BatchResponseImpl(callback.getResponses()); + try { + BatchRequest batch = newBatchRequest(messages, dryRun, callback); + batch.execute(); + return new BatchResponseImpl(callback.getResponses()); + } catch (HttpResponseException e) { + OutgoingHttpRequest req = new OutgoingHttpRequest( + HttpMethods.POST, MessagingBatchClient.FCM_BATCH_URL); + IncomingHttpResponse resp = new IncomingHttpResponse(e, req); + throw errorHandler.handleHttpResponseException(e, resp); + } catch (IOException e) { + throw errorHandler.handleIOException(e); + } } private BatchRequest newBatchRequest( List messages, boolean dryRun, MessagingBatchCallback callback) throws IOException { - BatchRequest batch = new BatchRequest( - requestFactory.getTransport(), getBatchRequestInitializer()); - batch.setBatchUrl(new GenericUrl(FCM_BATCH_URL)); - + BatchRequest batch = batchClient.batch(getBatchRequestInitializer()); final JsonObjectParser jsonParser = new JsonObjectParser(this.jsonFactory); final GenericUrl sendUrl = new GenericUrl(fcmSendUrl); for (Message message : messages) { @@ -186,36 +160,19 @@ private BatchRequest newBatchRequest( sendUrl, new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); request.setParser(jsonParser); - setCommonFcmHeaders(request.getHeaders()); + request.getHeaders().putAll(COMMON_HEADERS); batch.queue( request, MessagingServiceResponse.class, MessagingServiceErrorResponse.class, callback); } return batch; } - private void setCommonFcmHeaders(HttpHeaders headers) { - headers.set(API_FORMAT_VERSION_HEADER, "2"); - headers.set(CLIENT_VERSION_HEADER, clientVersion); - } - - private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { - MessagingServiceErrorResponse response = new MessagingServiceErrorResponse(); - if (e.getContent() != null) { - try { - JsonParser parser = jsonFactory.createJsonParser(e.getContent()); - parser.parseAndClose(response); - } catch (IOException ignored) { - // ignored - } - } - - return newException(response, e); - } - private HttpRequestInitializer getBatchRequestInitializer() { return new HttpRequestInitializer() { @Override public void initialize(HttpRequest request) throws IOException { + // Batch requests are not executed on the ErrorHandlingHttpClient. Therefore, they + // require some special handling at initialization. HttpRequestInitializer initializer = requestFactory.getInitializer(); if (initializer != null) { initializer.initialize(request); @@ -283,30 +240,6 @@ FirebaseMessagingClientImpl build() { } } - private static FirebaseMessagingException newException(MessagingServiceErrorResponse response) { - return newException(response, null); - } - - private static FirebaseMessagingException newException( - MessagingServiceErrorResponse response, @Nullable HttpResponseException e) { - String code = FCM_ERROR_CODES.get(response.getErrorCode()); - if (code == null) { - code = FirebaseMessaging.UNKNOWN_ERROR; - } - - String msg = response.getErrorMessage(); - if (Strings.isNullOrEmpty(msg)) { - if (e != null) { - msg = String.format("Unexpected HTTP response with status: %d; body: %s", - e.getStatusCode(), e.getContent()); - } else { - msg = String.format("Unexpected HTTP response: %s", response.toString()); - } - } - - return new FirebaseMessagingException(code, msg, e); - } - private static class MessagingBatchCallback implements BatchCallback { @@ -319,13 +252,97 @@ public void onSuccess( } @Override - public void onFailure( - MessagingServiceErrorResponse error, HttpHeaders responseHeaders) { - responses.add(SendResponse.fromException(newException(error))); + public void onFailure(MessagingServiceErrorResponse error, HttpHeaders responseHeaders) { + // We only specify error codes and message for these partial failures. Recall that these + // exceptions are never actually thrown, but only made accessible via SendResponse. + FirebaseException base = createFirebaseException(error); + FirebaseMessagingException exception = FirebaseMessagingException.withMessagingErrorCode( + base, error.getMessagingErrorCode()); + responses.add(SendResponse.fromException(exception)); } List getResponses() { return this.responses.build(); } + + private FirebaseException createFirebaseException(MessagingServiceErrorResponse error) { + String status = error.getStatus(); + ErrorCode errorCode = Strings.isNullOrEmpty(status) + ? ErrorCode.UNKNOWN : Enum.valueOf(ErrorCode.class, status); + + String msg = error.getErrorMessage(); + if (Strings.isNullOrEmpty(msg)) { + msg = String.format("Unexpected HTTP response: %s", error.toString()); + } + + return new FirebaseException(errorCode, msg, null); + } + } + + private static class MessagingErrorHandler + extends AbstractPlatformErrorHandler { + + private MessagingErrorHandler(JsonFactory jsonFactory) { + super(jsonFactory); + } + + @Override + protected FirebaseMessagingException createException(FirebaseException base) { + String response = getResponse(base); + MessagingServiceErrorResponse parsed = safeParse(response); + return FirebaseMessagingException.withMessagingErrorCode( + base, parsed.getMessagingErrorCode()); + } + + private String getResponse(FirebaseException base) { + if (base.getHttpResponse() == null) { + return null; + } + + return base.getHttpResponse().getContent(); + } + + private MessagingServiceErrorResponse safeParse(String response) { + if (!Strings.isNullOrEmpty(response)) { + try { + return jsonFactory.createJsonParser(response) + .parseAndClose(MessagingServiceErrorResponse.class); + } catch (IOException ignore) { + // Ignore any error that may occur while parsing the error response. The server + // may have responded with a non-json payload. + } + } + + return new MessagingServiceErrorResponse(); + } + } + + private static class MessagingBatchClient extends AbstractGoogleJsonClient { + + private static final String FCM_ROOT_URL = "https://fcm.googleapis.com"; + private static final String FCM_BATCH_PATH = "batch"; + private static final String FCM_BATCH_URL = String.format( + "%s/%s", FCM_ROOT_URL, FCM_BATCH_PATH); + + MessagingBatchClient(HttpTransport transport, JsonFactory jsonFactory) { + super(new Builder(transport, jsonFactory)); + } + + private MessagingBatchClient(Builder builder) { + super(builder); + } + + private static class Builder extends AbstractGoogleJsonClient.Builder { + Builder(HttpTransport transport, JsonFactory jsonFactory) { + super(transport, jsonFactory, FCM_ROOT_URL, "", null, false); + setBatchPath(FCM_BATCH_PATH); + setApplicationName("fire-admin-java"); + } + + @Override + public AbstractGoogleJsonClient build() { + return new MessagingBatchClient(this); + } + } } } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java index 5f57474ea..a3d92788a 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java @@ -16,26 +16,54 @@ package com.google.firebase.messaging; -import static com.google.common.base.Preconditions.checkArgument; - -import com.google.common.base.Strings; +import com.google.common.annotations.VisibleForTesting; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; + +public final class FirebaseMessagingException extends FirebaseException { -public class FirebaseMessagingException extends FirebaseException { + private final MessagingErrorCode errorCode; - private final String errorCode; + @VisibleForTesting + FirebaseMessagingException(@NonNull ErrorCode code, @NonNull String message) { + this(code, message, null, null, null); + } - FirebaseMessagingException(String errorCode, String message, Throwable cause) { - super(message, cause); - checkArgument(!Strings.isNullOrEmpty(errorCode)); + private FirebaseMessagingException( + @NonNull ErrorCode code, + @NonNull String message, + @Nullable Throwable cause, + @Nullable IncomingHttpResponse response, + @Nullable MessagingErrorCode errorCode) { + super(code, message, cause, response); this.errorCode = errorCode; } + static FirebaseMessagingException withMessagingErrorCode( + FirebaseException base, @Nullable MessagingErrorCode errorCode) { + return new FirebaseMessagingException( + base.getErrorCode(), + base.getMessage(), + base.getCause(), + base.getHttpResponse(), + errorCode); + } + + static FirebaseMessagingException withCustomMessage(FirebaseException base, String message) { + return new FirebaseMessagingException( + base.getErrorCode(), + message, + base.getCause(), + base.getHttpResponse(), + null); + } /** Returns an error code that may provide more information about the error. */ - @NonNull - public String getErrorCode() { + @Nullable + public MessagingErrorCode getMessagingErrorCode() { return errorCode; } } diff --git a/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java b/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java index 15f8158a5..5648fcf0c 100644 --- a/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java +++ b/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java @@ -16,27 +16,20 @@ package com.google.firebase.messaging; -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.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; -import com.google.api.client.http.json.JsonHttpContent; import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; -import com.google.api.client.json.JsonParser; import com.google.api.client.util.Key; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; +import com.google.firebase.internal.AbstractHttpErrorHandler; import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.ErrorHandlingHttpClient; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.Nullable; - import java.io.IOException; import java.util.List; import java.util.Map; @@ -53,18 +46,7 @@ final class InstanceIdClientImpl implements InstanceIdClient { private static final String IID_UNSUBSCRIBE_PATH = "iid/v1:batchRemove"; - static final Map IID_ERROR_CODES = - ImmutableMap.builder() - .put(400, "invalid-argument") - .put(401, "authentication-error") - .put(403, "authentication-error") - .put(500, FirebaseMessaging.INTERNAL_ERROR) - .put(503, "server-unavailable") - .build(); - - private final HttpRequestFactory requestFactory; - private final JsonFactory jsonFactory; - private final HttpResponseInterceptor responseInterceptor; + private final ErrorHandlingHttpClient requestFactory; InstanceIdClientImpl(HttpRequestFactory requestFactory, JsonFactory jsonFactory) { this(requestFactory, jsonFactory, null); @@ -74,9 +56,9 @@ final class InstanceIdClientImpl implements InstanceIdClient { HttpRequestFactory requestFactory, JsonFactory jsonFactory, @Nullable HttpResponseInterceptor responseInterceptor) { - this.requestFactory = checkNotNull(requestFactory); - this.jsonFactory = checkNotNull(jsonFactory); - this.responseInterceptor = responseInterceptor; + InstanceIdErrorHandler errorHandler = new InstanceIdErrorHandler(jsonFactory); + this.requestFactory = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler) + .setInterceptor(responseInterceptor); } static InstanceIdClientImpl fromApp(FirebaseApp app) { @@ -85,76 +67,32 @@ static InstanceIdClientImpl fromApp(FirebaseApp app) { app.getOptions().getJsonFactory()); } - @VisibleForTesting - HttpRequestFactory getRequestFactory() { - return requestFactory; - } - - @VisibleForTesting - JsonFactory getJsonFactory() { - return jsonFactory; - } - public TopicManagementResponse subscribeToTopic( String topic, List registrationTokens) throws FirebaseMessagingException { - try { - return sendInstanceIdRequest(topic, registrationTokens, IID_SUBSCRIBE_PATH); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); - } + return sendInstanceIdRequest(topic, registrationTokens, IID_SUBSCRIBE_PATH); } public TopicManagementResponse unsubscribeFromTopic( String topic, List registrationTokens) throws FirebaseMessagingException { - try { - return sendInstanceIdRequest(topic, registrationTokens, IID_UNSUBSCRIBE_PATH); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); - } + return sendInstanceIdRequest(topic, registrationTokens, IID_UNSUBSCRIBE_PATH); } private TopicManagementResponse sendInstanceIdRequest( - String topic, List registrationTokens, String path) throws IOException { + String topic, + List registrationTokens, + String path) throws FirebaseMessagingException { + String url = String.format("%s/%s", IID_HOST, path); Map payload = ImmutableMap.of( "to", getPrefixedTopic(topic), "registration_tokens", registrationTokens ); - HttpResponse response = null; - try { - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); - request.getHeaders().set("access_token_auth", "true"); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(responseInterceptor); - response = request.execute(); - - JsonParser parser = jsonFactory.createJsonParser(response.getContent()); - InstanceIdServiceResponse parsedResponse = new InstanceIdServiceResponse(); - parser.parse(parsedResponse); - return new TopicManagementResponse(parsedResponse.results); - } finally { - ApiClientUtils.disconnectQuietly(response); - } - } - private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { - InstanceIdServiceErrorResponse response = new InstanceIdServiceErrorResponse(); - if (e.getContent() != null) { - try { - JsonParser parser = jsonFactory.createJsonParser(e.getContent()); - parser.parseAndClose(response); - } catch (IOException ignored) { - // ignored - } - } - return newException(response, e); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(url, payload) + .addHeader("access_token_auth", "true"); + InstanceIdServiceResponse response = new InstanceIdServiceResponse(); + requestFactory.sendAndParse(request, response); + return new TopicManagementResponse(response.results); } private String getPrefixedTopic(String topic) { @@ -165,21 +103,6 @@ private String getPrefixedTopic(String topic) { } } - private static FirebaseMessagingException newException( - InstanceIdServiceErrorResponse response, HttpResponseException e) { - // Infer error code from HTTP status - String code = IID_ERROR_CODES.get(e.getStatusCode()); - if (code == null) { - code = FirebaseMessaging.UNKNOWN_ERROR; - } - String msg = response.error; - if (Strings.isNullOrEmpty(msg)) { - msg = String.format("Unexpected HTTP response with status: %d; body: %s", - e.getStatusCode(), e.getContent()); - } - return new FirebaseMessagingException(code, msg, e); - } - private static class InstanceIdServiceResponse { @Key("results") private List results; @@ -189,4 +112,54 @@ private static class InstanceIdServiceErrorResponse { @Key("error") private String error; } + + private static class InstanceIdErrorHandler + extends AbstractHttpErrorHandler { + + private final JsonFactory jsonFactory; + + InstanceIdErrorHandler(JsonFactory jsonFactory) { + this.jsonFactory = jsonFactory; + } + + @Override + protected FirebaseMessagingException createException(FirebaseException base) { + String message = getCustomMessage(base); + return FirebaseMessagingException.withCustomMessage(base, message); + } + + private String getCustomMessage(FirebaseException base) { + String response = getResponse(base); + InstanceIdServiceErrorResponse parsed = safeParse(response); + if (!Strings.isNullOrEmpty(parsed.error)) { + return "Error while calling the IID service: " + parsed.error; + } + + return base.getMessage(); + } + + private String getResponse(FirebaseException base) { + if (base.getHttpResponse() == null) { + return null; + } + + return base.getHttpResponse().getContent(); + } + + private InstanceIdServiceErrorResponse safeParse(String response) { + InstanceIdServiceErrorResponse parsed = new InstanceIdServiceErrorResponse(); + if (!Strings.isNullOrEmpty(response)) { + // Parse the error response from the IID service. + // Sample response: {"error": "error message text"} + try { + jsonFactory.createJsonParser(response).parse(parsed); + } catch (IOException ignore) { + // Ignore any error that may occur while parsing the error response. The server + // may have responded with a non-json payload. + } + } + + return parsed; + } + } } diff --git a/src/main/java/com/google/firebase/messaging/LightSettings.java b/src/main/java/com/google/firebase/messaging/LightSettings.java index 75a692090..e0e898374 100644 --- a/src/main/java/com/google/firebase/messaging/LightSettings.java +++ b/src/main/java/com/google/firebase/messaging/LightSettings.java @@ -61,7 +61,7 @@ private Builder() {} /** * Sets the lightSettingsColor value with a string. * - * @param lightSettingsColor LightSettingsColor specified in the {@code #rrggbb} format. + * @param color LightSettingsColor specified in the {@code #rrggbb} format. * @return This builder. */ public Builder setColorFromString(String color) { @@ -72,7 +72,7 @@ public Builder setColorFromString(String color) { /** * Sets the lightSettingsColor value in the light settings. * - * @param lightSettingsColor Color to be used in the light settings. + * @param color Color to be used in the light settings. * @return This builder. */ public Builder setColor(LightSettingsColor color) { diff --git a/src/main/java/com/google/firebase/messaging/MessagingErrorCode.java b/src/main/java/com/google/firebase/messaging/MessagingErrorCode.java new file mode 100644 index 000000000..b566befbd --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/MessagingErrorCode.java @@ -0,0 +1,43 @@ +package com.google.firebase.messaging; + +/** + * Error codes that can be raised by the Cloud Messaging APIs. + */ +public enum MessagingErrorCode { + + /** + * APNs certificate or web push auth key was invalid or missing. + */ + THIRD_PARTY_AUTH_ERROR, + + /** + * One or more arguments specified in the request were invalid. + */ + INVALID_ARGUMENT, + + /** + * Internal server error. + */ + INTERNAL, + + /** + * Sending limit exceeded for the message target. + */ + QUOTA_EXCEEDED, + + /** + * The authenticated sender ID is different from the sender ID for the registration token. + */ + SENDER_ID_MISMATCH, + + /** + * Cloud Messaging service is temporarily unavailable. + */ + UNAVAILABLE, + + /** + * App instance was unregistered from FCM. This usually means that the token used is no longer + * valid and a new one must be used. + */ + UNREGISTERED, +} diff --git a/src/main/java/com/google/firebase/messaging/Notification.java b/src/main/java/com/google/firebase/messaging/Notification.java index d9b2034ee..a8f48c2ef 100644 --- a/src/main/java/com/google/firebase/messaging/Notification.java +++ b/src/main/java/com/google/firebase/messaging/Notification.java @@ -33,33 +33,6 @@ public class Notification { @Key("image") private final String image; - /** - * Creates a new {@code Notification} using the given title and body. - * - * @param title Title of the notification. - * @param body Body of the notification. - * - * @deprecated Use {@link #Notification(Builder)} instead. - */ - public Notification(String title, String body) { - this(title, body, null); - } - - /** - * Creates a new {@code Notification} using the given title, body, and image. - * - * @param title Title of the notification. - * @param body Body of the notification. - * @param imageUrl URL of the image that is going to be displayed in the notification. - * - * @deprecated Use {@link #Notification(Builder)} instead. - */ - public Notification(String title, String body, String imageUrl) { - this.title = title; - this.body = body; - this.image = imageUrl; - } - private Notification(Builder builder) { this.title = builder.title; this.body = builder.body; @@ -67,9 +40,9 @@ private Notification(Builder builder) { } /** - * Creates a new {@link Notification.Builder}. + * Creates a new {@link Builder}. * - * @return A {@link Notification.Builder} instance. + * @return A {@link Builder} instance. */ public static Builder builder() { return new Builder(); diff --git a/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java b/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java index d63f3af95..7c21199cc 100644 --- a/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java +++ b/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java @@ -2,14 +2,28 @@ import com.google.api.client.json.GenericJson; import com.google.api.client.util.Key; +import com.google.common.collect.ImmutableMap; import com.google.firebase.internal.Nullable; +import com.google.firebase.messaging.MessagingErrorCode; import java.util.List; import java.util.Map; /** * The DTO for parsing error responses from the FCM service. */ -public class MessagingServiceErrorResponse extends GenericJson { +public final class MessagingServiceErrorResponse extends GenericJson { + + private static final Map MESSAGING_ERROR_CODES = + ImmutableMap.builder() + .put("APNS_AUTH_ERROR", MessagingErrorCode.THIRD_PARTY_AUTH_ERROR) + .put("INTERNAL", MessagingErrorCode.INTERNAL) + .put("INVALID_ARGUMENT", MessagingErrorCode.INVALID_ARGUMENT) + .put("QUOTA_EXCEEDED", MessagingErrorCode.QUOTA_EXCEEDED) + .put("SENDER_ID_MISMATCH", MessagingErrorCode.SENDER_ID_MISMATCH) + .put("THIRD_PARTY_AUTH_ERROR", MessagingErrorCode.THIRD_PARTY_AUTH_ERROR) + .put("UNAVAILABLE", MessagingErrorCode.UNAVAILABLE) + .put("UNREGISTERED", MessagingErrorCode.UNREGISTERED) + .build(); private static final String FCM_ERROR_TYPE = "type.googleapis.com/google.firebase.fcm.v1.FcmError"; @@ -17,23 +31,35 @@ public class MessagingServiceErrorResponse extends GenericJson { @Key("error") private Map error; + public String getStatus() { + if (error == null) { + return null; + } + + return (String) error.get("status"); + } + + @Nullable - public String getErrorCode() { + public MessagingErrorCode getMessagingErrorCode() { if (error == null) { return null; } + Object details = error.get("details"); - if (details != null && details instanceof List) { + if (details instanceof List) { for (Object detail : (List) details) { if (detail instanceof Map) { Map detailMap = (Map) detail; if (FCM_ERROR_TYPE.equals(detailMap.get("@type"))) { - return (String) detailMap.get("errorCode"); + String errorCode = (String) detailMap.get("errorCode"); + return MESSAGING_ERROR_CODES.get(errorCode); } } } } - return (String) error.get("status"); + + return null; } @Nullable @@ -41,6 +67,7 @@ public String getErrorMessage() { if (error != null) { return (String) error.get("message"); } + return null; } } diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementException.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementException.java index fa939b0c4..580ae76f6 100644 --- a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementException.java +++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementException.java @@ -15,18 +15,27 @@ package com.google.firebase.projectmanagement; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseException; -import com.google.firebase.internal.Nullable; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.database.annotations.Nullable; +import com.google.firebase.internal.NonNull; /** * An exception encountered while interacting with the Firebase Project Management Service. */ -public class FirebaseProjectManagementException extends FirebaseException { - FirebaseProjectManagementException(String detailMessage) { - this(detailMessage, null); +public final class FirebaseProjectManagementException extends FirebaseException { + + FirebaseProjectManagementException(@NonNull FirebaseException base) { + this(base, base.getMessage()); + } + + FirebaseProjectManagementException(@NonNull FirebaseException base, @NonNull String message) { + super(base.getErrorCode(), message, base.getCause(), base.getHttpResponse()); } - FirebaseProjectManagementException(String detailMessage, @Nullable Throwable cause) { - super(detailMessage, cause); + FirebaseProjectManagementException( + @NonNull ErrorCode code, @NonNull String message, @Nullable IncomingHttpResponse response) { + super(code, message, null, response); } } diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java index 8abced696..0f9eb3b55 100644 --- a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java +++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java @@ -31,8 +31,10 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.IncomingHttpResponse; import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.CallableOperation; import java.nio.charset.StandardCharsets; @@ -56,7 +58,6 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS private final FirebaseApp app; private final Sleeper sleeper; private final Scheduler scheduler; - private final HttpRequestFactory requestFactory; private final HttpHelper httpHelper; private final CreateAndroidAppFromAppIdFunction createAndroidAppFromAppIdFunction = @@ -78,15 +79,9 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS this.app = checkNotNull(app); this.sleeper = checkNotNull(sleeper); this.scheduler = checkNotNull(scheduler); - this.requestFactory = checkNotNull(requestFactory); this.httpHelper = new HttpHelper(app.getOptions().getJsonFactory(), requestFactory); } - @VisibleForTesting - HttpRequestFactory getRequestFactory() { - return requestFactory; - } - @VisibleForTesting void setInterceptor(HttpResponseInterceptor interceptor) { httpHelper.setInterceptor(interceptor); @@ -318,14 +313,14 @@ protected String execute() throws FirebaseProjectManagementException { payloadBuilder.put("display_name", displayName); } OperationResponse operationResponseInstance = new OperationResponse(); - httpHelper.makePostRequest( + IncomingHttpResponse response = httpHelper.makePostRequest( url, payloadBuilder.build(), operationResponseInstance, projectId, "Project ID"); if (Strings.isNullOrEmpty(operationResponseInstance.name)) { - throw HttpHelper.createFirebaseProjectManagementException( + String message = buildMessage( namespace, "Bundle ID", - "Unable to create App: server returned null operation name.", - /* cause= */ null); + "Unable to create App: server returned null operation name."); + throw new FirebaseProjectManagementException(ErrorCode.INTERNAL, message, response); } return operationResponseInstance.name; } @@ -341,7 +336,8 @@ private String pollOperation(String projectId, String operationName) * Math.pow(POLL_EXPONENTIAL_BACKOFF_FACTOR, currentAttempt)); sleepOrThrow(projectId, delayMillis); OperationResponse operationResponseInstance = new OperationResponse(); - httpHelper.makeGetRequest(url, operationResponseInstance, projectId, "Project ID"); + IncomingHttpResponse response = httpHelper.makeGetRequest( + url, operationResponseInstance, projectId, "Project ID"); if (!operationResponseInstance.done) { continue; } @@ -349,19 +345,20 @@ private String pollOperation(String projectId, String operationName) // or 'error' is set. if (operationResponseInstance.response == null || Strings.isNullOrEmpty(operationResponseInstance.response.appId)) { - throw HttpHelper.createFirebaseProjectManagementException( + String message = buildMessage( projectId, "Project ID", - "Unable to create App: internal server error.", - /* cause= */ null); + "Unable to create App: internal server error."); + throw new FirebaseProjectManagementException(ErrorCode.INTERNAL, message, response); } return operationResponseInstance.response.appId; } - throw HttpHelper.createFirebaseProjectManagementException( + + String message = buildMessage( projectId, "Project ID", - "Unable to create App: deadline exceeded.", - /* cause= */ null); + "Unable to create App: deadline exceeded."); + throw new FirebaseProjectManagementException(ErrorCode.DEADLINE_EXCEEDED, message, null); } /** @@ -420,19 +417,22 @@ private WaitOperationRunnable( public void run() { String url = String.format("%s/v1/%s", FIREBASE_PROJECT_MANAGEMENT_URL, operationName); OperationResponse operationResponseInstance = new OperationResponse(); + IncomingHttpResponse httpResponse; try { - httpHelper.makeGetRequest(url, operationResponseInstance, projectId, "Project ID"); + httpResponse = 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, + String message = buildMessage(projectId, "Project ID", - "Unable to create App: deadline exceeded.", - /* cause= */ null)); + "Unable to create App: deadline exceeded."); + FirebaseProjectManagementException exception = new FirebaseProjectManagementException( + ErrorCode.DEADLINE_EXCEEDED, message, httpResponse); + settableFuture.setException(exception); } else { long delayMillis = (long) ( POLL_BASE_WAIT_TIME_MILLIS @@ -451,11 +451,12 @@ public void run() { // or 'error' is set. if (operationResponseInstance.response == null || Strings.isNullOrEmpty(operationResponseInstance.response.appId)) { - settableFuture.setException(HttpHelper.createFirebaseProjectManagementException( - projectId, + String message = buildMessage(projectId, "Project ID", - "Unable to create App: internal server error.", - /* cause= */ null)); + "Unable to create App: internal server error."); + FirebaseProjectManagementException exception = new FirebaseProjectManagementException( + ErrorCode.INTERNAL, message, httpResponse); + settableFuture.setException(exception); } else { settableFuture.set(operationResponseInstance.response.appId); } @@ -765,14 +766,17 @@ private void sleepOrThrow(String projectId, long delayMillis) try { sleeper.sleep(delayMillis); } catch (InterruptedException e) { - throw HttpHelper.createFirebaseProjectManagementException( - projectId, + String message = buildMessage(projectId, "Project ID", - "Unable to create App: exponential backoff interrupted.", - /* cause= */ null); + "Unable to create App: exponential backoff interrupted."); + throw new FirebaseProjectManagementException(ErrorCode.ABORTED, message, null); } } + private String buildMessage(String resourceId, String resourceIdName, String description) { + return String.format("%s \"%s\": %s", resourceIdName, resourceId, description); + } + /* Helper types. */ private interface CreateAppFromAppIdFunction extends ApiFunction {} diff --git a/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java b/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java index 2143c1453..fb29f7b86 100644 --- a/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java +++ b/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java @@ -16,84 +16,57 @@ 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.HttpMethods; 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.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.internal.AbstractPlatformErrorHandler; +import com.google.firebase.internal.ErrorHandlingHttpClient; +import com.google.firebase.internal.HttpRequestInfo; 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(); + +final class HttpHelper { + 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; + private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion(); + + private final ErrorHandlingHttpClient httpClient; HttpHelper(JsonFactory jsonFactory, HttpRequestFactory requestFactory) { - this.jsonFactory = jsonFactory; - this.requestFactory = requestFactory; + ProjectManagementErrorHandler errorHandler = new ProjectManagementErrorHandler(jsonFactory); + this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler); } void setInterceptor(HttpResponseInterceptor interceptor) { - this.interceptor = interceptor; + httpClient.setInterceptor(interceptor); } - void makeGetRequest( + IncomingHttpResponse 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); - } + return makeRequest( + HttpRequestInfo.buildGetRequest(url), + parsedResponseInstance, + requestIdentifier, + requestIdentifierDescription); } - void makePostRequest( + IncomingHttpResponse 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); - } + return makeRequest( + HttpRequestInfo.buildJsonPostRequest(url, payload), + parsedResponseInstance, + requestIdentifier, + requestIdentifierDescription); } void makePatchRequest( @@ -102,15 +75,11 @@ void makePatchRequest( 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); - } + makeRequest( + HttpRequestInfo.buildJsonRequest(HttpMethods.PATCH, url, payload), + parsedResponseInstance, + requestIdentifier, + requestIdentifierDescription); } void makeDeleteRequest( @@ -118,69 +87,40 @@ void makeDeleteRequest( 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); - } + makeRequest( + HttpRequestInfo.buildDeleteRequest(url), + parsedResponseInstance, + requestIdentifier, + requestIdentifierDescription); } - void makeRequest( - HttpRequest baseRequest, + private IncomingHttpResponse makeRequest( + HttpRequestInfo 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); + baseRequest.addHeader(CLIENT_VERSION_HEADER, CLIENT_VERSION); + IncomingHttpResponse response = httpClient.send(baseRequest); + httpClient.parse(response, parsedResponseInstance); + return response; + } catch (FirebaseProjectManagementException e) { + String message = String.format( + "%s \"%s\": %s", requestIdentifierDescription, requestIdentifier, e.getMessage()); + throw new FirebaseProjectManagementException(e, message); } } - private static void disconnectQuietly(HttpResponse response) { - if (response != null) { - try { - response.disconnect(); - } catch (IOException ignored) { - // Ignored. - } - } - } + private static class ProjectManagementErrorHandler + extends AbstractPlatformErrorHandler { - 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); - } + ProjectManagementErrorHandler(JsonFactory jsonFactory) { + super(jsonFactory); } - 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); + @Override + protected FirebaseProjectManagementException createException(FirebaseException base) { + return new FirebaseProjectManagementException(base); + } } } diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java index eee9efdb8..d481e3c0e 100644 --- a/src/test/java/com/google/firebase/FirebaseAppTest.java +++ b/src/test/java/com/google/firebase/FirebaseAppTest.java @@ -35,13 +35,11 @@ import com.google.auth.oauth2.OAuth2Credentials.CredentialsChangedListener; import com.google.common.base.Defaults; import com.google.common.base.Strings; -import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.firebase.FirebaseApp.TokenRefresher; -import com.google.firebase.FirebaseOptions.Builder; import com.google.firebase.database.FirebaseDatabase; import com.google.firebase.testing.FirebaseAppRule; import com.google.firebase.testing.ServiceAccount; @@ -74,7 +72,7 @@ public class FirebaseAppTest { private static final FirebaseOptions OPTIONS = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .build(); @@ -110,7 +108,7 @@ public void testGetInstancePersistedNotInitialized() { @Test public void testGetProjectIdFromOptions() { - FirebaseOptions options = new FirebaseOptions.Builder(OPTIONS) + FirebaseOptions options = OPTIONS.toBuilder() .setProjectId("explicit-project-id") .build(); FirebaseApp app = FirebaseApp.initializeApp(options, "myApp"); @@ -131,7 +129,7 @@ public void testGetProjectIdFromEnvironment() { for (String variable : variables) { String gcloudProject = System.getenv(variable); TestUtils.setEnvironmentVariables(ImmutableMap.of(variable, "project-id-1")); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .build(); try { @@ -155,7 +153,7 @@ public void testProjectIdEnvironmentVariablePrecedence() { TestUtils.setEnvironmentVariables(ImmutableMap.of( "GCLOUD_PROJECT", "project-id-1", "GOOGLE_CLOUD_PROJECT", "project-id-2")); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .build(); try { @@ -239,7 +237,7 @@ public void testGetNullApp() { @Test public void testToString() throws IOException { FirebaseOptions options = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .build(); FirebaseApp app = FirebaseApp.initializeApp(options, "app"); @@ -461,14 +459,13 @@ public void testTokenRefresherStateMachine() { @Test public void testAppWithAuthVariableOverrides() { Map authVariableOverrides = ImmutableMap.of("uid", "uid1"); - FirebaseOptions options = - new FirebaseOptions.Builder(getMockCredentialOptions()) - .setDatabaseAuthVariableOverride(authVariableOverrides) - .build(); + FirebaseOptions options = getMockCredentialOptions().toBuilder() + .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()); + Assert.assertFalse(token.isEmpty()); } @Test(expected = IllegalArgumentException.class) @@ -579,16 +576,6 @@ public void testFirebaseConfigStringIgnoresInvalidKey() { assertEquals("hipster-chat-mock", firebaseApp.getOptions().getProjectId()); } - @Test(expected = IllegalArgumentException.class) - public void testFirebaseExceptionNullDetail() { - new FirebaseException(null); - } - - @Test(expected = IllegalArgumentException.class) - public void testFirebaseExceptionEmptyDetail() { - new FirebaseException(""); - } - @Test public void testFirebaseAppCreationWithEmptySupplier() { FirebaseApp.initializeApp(FirebaseOptions.builder() @@ -609,7 +596,7 @@ private static void setFirebaseConfigEnvironmentVariable(String configJSON) { } private static FirebaseOptions getMockCredentialOptions() { - return new Builder().setCredentials(new MockGoogleCredentials()).build(); + return FirebaseOptions.builder().setCredentials(new MockGoogleCredentials()).build(); } private static void invokePublicInstanceMethodWithDefaultValues(Object instance, Method method) diff --git a/src/test/java/com/google/firebase/FirebaseExceptionTest.java b/src/test/java/com/google/firebase/FirebaseExceptionTest.java new file mode 100644 index 000000000..efe4e39fa --- /dev/null +++ b/src/test/java/com/google/firebase/FirebaseExceptionTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2020 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpStatusCodes; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.firebase.testing.TestUtils; +import java.io.IOException; +import org.junit.Test; + +@SuppressWarnings("ThrowableNotThrown") +public class FirebaseExceptionTest { + + @Test(expected = NullPointerException.class) + public void testFirebaseExceptionWithoutErrorCode() { + new FirebaseException( + null, + "Test error", + null, + null); + } + + @Test(expected = IllegalArgumentException.class) + public void testFirebaseExceptionWithNullMessage() { + new FirebaseException( + ErrorCode.INTERNAL, + null, + null, + null); + } + + @Test(expected = IllegalArgumentException.class) + public void testFirebaseExceptionWithEmptyMessage() { + new FirebaseException( + ErrorCode.INTERNAL, + "", + null, + null); + } + + @Test + public void testFirebaseExceptionWithoutResponseAndCause() { + FirebaseException exception = new FirebaseException( + ErrorCode.INTERNAL, + "Test error", + null, + null); + + assertEquals(ErrorCode.INTERNAL, exception.getErrorCode()); + assertEquals("Test error", exception.getMessage()); + assertNull(exception.getHttpResponse()); + assertNull(exception.getCause()); + } + + @Test + public void testFirebaseExceptionWithResponse() throws IOException { + HttpResponseException httpError = createHttpResponseException(); + OutgoingHttpRequest request = new OutgoingHttpRequest( + "GET", "https://firebase.google.com"); + IncomingHttpResponse response = new IncomingHttpResponse(httpError, request); + + FirebaseException exception = new FirebaseException( + ErrorCode.INTERNAL, + "Test error", + null, + response); + + assertEquals(ErrorCode.INTERNAL, exception.getErrorCode()); + assertEquals("Test error", exception.getMessage()); + assertSame(response, exception.getHttpResponse()); + assertNull(exception.getCause()); + } + + @Test + public void testFirebaseExceptionWithCause() { + Exception cause = new Exception("root cause"); + + FirebaseException exception = new FirebaseException( + ErrorCode.INTERNAL, + "Test error", + cause); + + assertEquals(ErrorCode.INTERNAL, exception.getErrorCode()); + assertEquals("Test error", exception.getMessage()); + assertNull(exception.getHttpResponse()); + assertSame(cause, exception.getCause()); + } + + private HttpResponseException createHttpResponseException() throws IOException { + MockLowLevelHttpResponse lowLevelResponse = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent("{}"); + MockLowLevelHttpRequest lowLevelRequest = new MockLowLevelHttpRequest() + .setResponse(lowLevelResponse); + HttpRequest request = TestUtils.createRequest(lowLevelRequest); + try { + request.execute(); + throw new IOException("HttpResponseException not thrown"); + } catch (HttpResponseException e) { + return e; + } + } +} diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java index a3dac26fd..c74215beb 100644 --- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java +++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java @@ -17,14 +17,13 @@ package com.google.firebase; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; 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 com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.gson.GsonFactory; import com.google.auth.oauth2.AccessToken; @@ -49,7 +48,7 @@ public class FirebaseOptionsTest { private static final String FIREBASE_PROJECT_ID = "explicit-project-id"; private static final FirebaseOptions ALL_VALUES_OPTIONS = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setDatabaseUrl(FIREBASE_DB_URL) .setStorageBucket(FIREBASE_STORAGE_BUCKET) .setProjectId(FIREBASE_PROJECT_ID) @@ -76,11 +75,9 @@ protected ThreadFactory getThreadFactory() { public void createOptionsWithAllValuesSet() throws IOException { GsonFactory jsonFactory = new GsonFactory(); NetHttpTransport httpTransport = new NetHttpTransport(); - FirestoreOptions firestoreOptions = FirestoreOptions.newBuilder() - .setTimestampsInSnapshotsEnabled(true) - .build(); + FirestoreOptions firestoreOptions = FirestoreOptions.newBuilder().build(); FirebaseOptions firebaseOptions = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setDatabaseUrl(FIREBASE_DB_URL) .setStorageBucket(FIREBASE_STORAGE_BUCKET) .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) @@ -106,14 +103,14 @@ public void createOptionsWithAllValuesSet() throws IOException { assertNotNull(credentials); assertTrue(credentials instanceof ServiceAccountCredentials); assertEquals( - GoogleCredential.fromStream(ServiceAccount.EDITOR.asStream()).getServiceAccountId(), + ServiceAccount.EDITOR.getEmail(), ((ServiceAccountCredentials) credentials).getClientEmail()); } @Test public void createOptionsWithOnlyMandatoryValuesSet() throws IOException { FirebaseOptions firebaseOptions = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .build(); assertNotNull(firebaseOptions.getJsonFactory()); @@ -128,7 +125,7 @@ public void createOptionsWithOnlyMandatoryValuesSet() throws IOException { assertNotNull(credentials); assertTrue(credentials instanceof ServiceAccountCredentials); assertEquals( - GoogleCredential.fromStream(ServiceAccount.EDITOR.asStream()).getServiceAccountId(), + ServiceAccount.EDITOR.getEmail(), ((ServiceAccountCredentials) credentials).getClientEmail()); assertNull(firebaseOptions.getFirestoreOptions()); } @@ -136,7 +133,7 @@ public void createOptionsWithOnlyMandatoryValuesSet() throws IOException { @Test public void createOptionsWithCustomFirebaseCredential() { FirebaseOptions firebaseOptions = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(new GoogleCredentials() { @Override public AccessToken refreshAccessToken() { @@ -156,17 +153,17 @@ public AccessToken refreshAccessToken() { @Test(expected = NullPointerException.class) public void createOptionsWithCredentialMissing() { - new FirebaseOptions.Builder().build().getCredentials(); + FirebaseOptions.builder().build().getCredentials(); } @Test(expected = NullPointerException.class) public void createOptionsWithNullCredentials() { - new FirebaseOptions.Builder().setCredentials((GoogleCredentials) null).build(); + FirebaseOptions.builder().setCredentials((GoogleCredentials) null).build(); } @Test(expected = IllegalArgumentException.class) public void createOptionsWithStorageBucketUrl() throws IOException { - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setStorageBucket("gs://mock-storage-bucket") .build(); @@ -174,7 +171,7 @@ public void createOptionsWithStorageBucketUrl() throws IOException { @Test(expected = NullPointerException.class) public void createOptionsWithNullThreadManager() { - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .setThreadManager(null) .build(); @@ -182,7 +179,7 @@ public void createOptionsWithNullThreadManager() { @Test public void checkToBuilderCreatesNewEquivalentInstance() { - FirebaseOptions allValuesOptionsCopy = new FirebaseOptions.Builder(ALL_VALUES_OPTIONS).build(); + FirebaseOptions allValuesOptionsCopy = ALL_VALUES_OPTIONS.toBuilder().build(); assertNotSame(ALL_VALUES_OPTIONS, allValuesOptionsCopy); assertEquals(ALL_VALUES_OPTIONS.getCredentials(), allValuesOptionsCopy.getCredentials()); assertEquals(ALL_VALUES_OPTIONS.getDatabaseUrl(), allValuesOptionsCopy.getDatabaseUrl()); @@ -198,7 +195,7 @@ public void checkToBuilderCreatesNewEquivalentInstance() { @Test(expected = IllegalArgumentException.class) public void createOptionsWithInvalidConnectTimeout() { - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .setConnectTimeout(-1) .build(); @@ -206,7 +203,7 @@ public void createOptionsWithInvalidConnectTimeout() { @Test(expected = IllegalArgumentException.class) public void createOptionsWithInvalidReadTimeout() { - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .setReadTimeout(-1) .build(); @@ -216,14 +213,14 @@ public void createOptionsWithInvalidReadTimeout() { public void testNotEquals() throws IOException { GoogleCredentials credentials = GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream()); FirebaseOptions options1 = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseOptions options2 = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(credentials) .setDatabaseUrl("https://test.firebaseio.com") .build(); - assertFalse(options1.equals(options2)); + assertNotEquals(options1, options2); } } diff --git a/src/test/java/com/google/firebase/IncomingHttpResponseTest.java b/src/test/java/com/google/firebase/IncomingHttpResponseTest.java new file mode 100644 index 000000000..4da63d12b --- /dev/null +++ b/src/test/java/com/google/firebase/IncomingHttpResponseTest.java @@ -0,0 +1,141 @@ +/* + * Copyright 2020 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpStatusCodes; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.testing.TestUtils; +import java.io.IOException; +import java.util.Map; +import org.junit.Test; + +public class IncomingHttpResponseTest { + + private static final String TEST_URL = "https://firebase.google.com/response"; + private static final OutgoingHttpRequest REQUEST = new OutgoingHttpRequest("GET", TEST_URL); + private static final Map RESPONSE_HEADERS = + ImmutableMap.of("x-firebase-client", ImmutableList.of("test-version")); + private static final String RESPONSE_BODY = "test response"; + + @Test(expected = NullPointerException.class) + public void testNullHttpResponse() { + new IncomingHttpResponse(null, "content"); + } + + @Test + public void testNullHttpResponseException() throws IOException { + try { + new IncomingHttpResponse(null, REQUEST); + fail("No exception thrown for null HttpResponseException"); + } catch (NullPointerException ignore) { + // expected + } + + HttpRequest request = createHttpRequest(); + try { + new IncomingHttpResponse(null, request); + fail("No exception thrown for null HttpResponseException"); + } catch (NullPointerException ignore) { + // expected + } + } + + @Test + public void testIncomingHttpResponse() throws IOException { + HttpResponseException httpError = createHttpResponseException(); + + IncomingHttpResponse response = new IncomingHttpResponse(httpError, REQUEST); + + assertEquals(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, response.getStatusCode()); + assertEquals(RESPONSE_BODY, response.getContent()); + assertEquals(RESPONSE_HEADERS, response.getHeaders()); + assertFalse(response.getHeaders().isEmpty()); + assertSame(REQUEST, response.getRequest()); + } + + @Test + public void testIncomingHttpResponseWithRequest() throws IOException { + HttpResponseException httpError = createHttpResponseException(); + HttpRequest httpRequest = createHttpRequest(); + + IncomingHttpResponse response = new IncomingHttpResponse(httpError, httpRequest); + + assertEquals(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, response.getStatusCode()); + assertEquals(RESPONSE_BODY, response.getContent()); + assertEquals(RESPONSE_HEADERS, response.getHeaders()); + OutgoingHttpRequest request = response.getRequest(); + assertEquals(HttpMethods.POST, request.getMethod()); + assertEquals(TEST_URL, request.getUrl()); + } + + @Test + public void testIncomingHttpResponseWithResponse() throws IOException { + HttpResponse httpResponse = createHttpResponse(); + + IncomingHttpResponse response = new IncomingHttpResponse(httpResponse, RESPONSE_BODY); + + assertEquals(HttpStatusCodes.STATUS_CODE_OK, response.getStatusCode()); + assertEquals(RESPONSE_BODY, response.getContent()); + assertTrue(response.getHeaders().isEmpty()); + OutgoingHttpRequest request = response.getRequest(); + assertEquals(HttpMethods.POST, request.getMethod()); + assertEquals(TEST_URL, request.getUrl()); + } + + private HttpResponseException createHttpResponseException() throws IOException { + MockLowLevelHttpResponse lowLevelResponse = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .addHeader("X-Firebase-Client", "test-version") + .setContent(RESPONSE_BODY); + MockLowLevelHttpRequest lowLevelRequest = new MockLowLevelHttpRequest() + .setResponse(lowLevelResponse); + HttpRequest request = TestUtils.createRequest(lowLevelRequest, new GenericUrl(TEST_URL)); + try { + request.execute(); + throw new IOException("HttpResponseException not thrown"); + } catch (HttpResponseException e) { + return e; + } + } + + private HttpRequest createHttpRequest() throws IOException { + MockLowLevelHttpResponse lowLevelResponse = new MockLowLevelHttpResponse() + .setContent("{}"); + MockLowLevelHttpRequest lowLevelRequest = new MockLowLevelHttpRequest() + .setResponse(lowLevelResponse); + return TestUtils.createRequest(lowLevelRequest, new GenericUrl(TEST_URL)); + } + + private HttpResponse createHttpResponse() throws IOException { + HttpRequest request = createHttpRequest(); + return request.execute(); + } +} diff --git a/src/test/java/com/google/firebase/OutgoingHttpRequestTest.java b/src/test/java/com/google/firebase/OutgoingHttpRequestTest.java new file mode 100644 index 000000000..792d621f5 --- /dev/null +++ b/src/test/java/com/google/firebase/OutgoingHttpRequestTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import org.junit.Test; + +public class OutgoingHttpRequestTest { + + private static final String TEST_URL = "https://firebase.google.com/request"; + + @Test(expected = NullPointerException.class) + public void testNullHttpRequest() { + new OutgoingHttpRequest(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testNullMethod() { + new OutgoingHttpRequest(null, TEST_URL); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyMethod() { + new OutgoingHttpRequest("", TEST_URL); + } + + @Test(expected = IllegalArgumentException.class) + public void testNullUrl() { + new OutgoingHttpRequest(HttpMethods.GET, null); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyUrl() { + new OutgoingHttpRequest(HttpMethods.GET, ""); + } + + @Test + public void testOutgoingHttpRequest() { + OutgoingHttpRequest request = new OutgoingHttpRequest(HttpMethods.GET, TEST_URL); + + assertEquals(HttpMethods.GET, request.getMethod()); + assertEquals(TEST_URL, request.getUrl()); + assertNull(request.getContent()); + assertTrue(request.getHeaders().isEmpty()); + } + + @Test + public void testOutgoingHttpRequestWithContent() throws IOException { + JsonHttpContent streamingContent = new JsonHttpContent( + Utils.getDefaultJsonFactory(), + ImmutableMap.of("key", "value")); + HttpRequest httpRequest = new MockHttpTransport().createRequestFactory() + .buildPostRequest(new GenericUrl(TEST_URL), streamingContent); + httpRequest.getHeaders().set("X-Firebase-Client", "test-version"); + + OutgoingHttpRequest request = new OutgoingHttpRequest(httpRequest); + + assertEquals(HttpMethods.POST, request.getMethod()); + assertEquals(TEST_URL, request.getUrl()); + assertSame(streamingContent, request.getContent()); + assertEquals("test-version", request.getHeaders().get("x-firebase-client")); + } +} diff --git a/src/test/java/com/google/firebase/ThreadManagerTest.java b/src/test/java/com/google/firebase/ThreadManagerTest.java index d8689a4da..22401de30 100644 --- a/src/test/java/com/google/firebase/ThreadManagerTest.java +++ b/src/test/java/com/google/firebase/ThreadManagerTest.java @@ -187,8 +187,7 @@ public void testAppLifecycleWithServiceCall() { } @Test - public void testAppLifecycleWithMultipleServiceCalls() - throws ExecutionException, InterruptedException { + public void testAppLifecycleWithMultipleServiceCalls() { MockThreadManager threadManager = new MockThreadManager(executor); // Initializing an app should initialize the executor. @@ -235,7 +234,7 @@ public void testAppLifecycleWithMultipleServiceCalls() } private FirebaseOptions buildOptions(ThreadManager threadManager) { - return new FirebaseOptions.Builder() + return FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .setProjectId("mock-project-id") .setThreadManager(threadManager) @@ -278,7 +277,7 @@ private static class Event { private final FirebaseApp app; private ExecutorService executor; - public Event(int type, @Nullable FirebaseApp app, @Nullable ExecutorService executor) { + Event(int type, @Nullable FirebaseApp app, @Nullable ExecutorService executor) { this.type = type; this.app = app; this.executor = executor; diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 803591d81..35fa21d4d 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -43,6 +43,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.io.BaseEncoding; import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.ImplFirebaseTrampolines; @@ -50,7 +51,6 @@ import com.google.firebase.auth.UserTestUtils.RandomUser; import com.google.firebase.auth.UserTestUtils.TemporaryUser; import com.google.firebase.auth.hash.Scrypt; -import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.internal.Nullable; import com.google.firebase.testing.IntegrationTestUtils; import java.io.IOException; @@ -96,8 +96,14 @@ public void testGetNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals( + "No user record found for the provided user ID: non.existing", + authException.getMessage()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -108,8 +114,14 @@ public void testGetNonExistingUserByEmail() throws Exception { fail("No error thrown for non existing email"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals( + "No user record found for the provided email: non.existing@definitely.non.existing", + authException.getMessage()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -120,8 +132,14 @@ public void testUpdateNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals( + "No user record found for the given identifier (USER_NOT_FOUND).", + authException.getMessage()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertNotNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -132,8 +150,14 @@ public void testDeleteNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals( + "No user record found for the given identifier (USER_NOT_FOUND).", + authException.getMessage()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertNotNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -313,43 +337,39 @@ public void testUserLifecycle() throws Exception { @Test public void testLastRefreshTime() throws Exception { RandomUser user = UserTestUtils.generateRandomUserInfo(); - UserRecord newUserRecord = auth.createUser(new UserRecord.CreateRequest() - .setUid(user.getUid()) - .setEmail(user.getEmail()) - .setEmailVerified(false) - .setPassword("password")); + UserRecord newUserRecord = temporaryUser.create(new UserRecord.CreateRequest() + .setUid(user.getUid()) + .setEmail(user.getEmail()) + .setEmailVerified(false) + .setPassword("password")); - try { - // New users should not have a lastRefreshTimestamp set. - assertEquals(0, newUserRecord.getUserMetadata().getLastRefreshTimestamp()); - - // Login to cause the lastRefreshTimestamp to be set. - signInWithPassword(newUserRecord.getEmail(), "password"); - - // Attempt to retrieve the user 3 times (with a small delay between each - // attempt). Occassionally, this call retrieves the user data without the - // lastLoginTime/lastRefreshTime set; possibly because it's hitting a - // different server than the login request uses. - UserRecord userRecord = null; - for (int i = 0; i < 3; i++) { - userRecord = auth.getUser(newUserRecord.getUid()); - - if (userRecord.getUserMetadata().getLastRefreshTimestamp() != 0) { - break; - } + // New users should not have a lastRefreshTimestamp set. + assertEquals(0, newUserRecord.getUserMetadata().getLastRefreshTimestamp()); - TimeUnit.SECONDS.sleep((long)Math.pow(2, i)); + // Login to cause the lastRefreshTimestamp to be set. + signInWithPassword(newUserRecord.getEmail(), "password"); + + // Attempt to retrieve the user 3 times (with a small delay between each + // attempt). Occasionally, this call retrieves the user data without the + // lastLoginTime/lastRefreshTime set; possibly because it's hitting a + // different server than the login request uses. + UserRecord userRecord = null; + for (int i = 0; i < 3; i++) { + userRecord = auth.getUser(newUserRecord.getUid()); + + if (userRecord.getUserMetadata().getLastRefreshTimestamp() != 0) { + break; } - // Ensure the lastRefreshTimestamp is approximately "now" (with a tollerance of 10 minutes). - long now = System.currentTimeMillis(); - long tollerance = TimeUnit.MINUTES.toMillis(10); - long lastRefreshTimestamp = userRecord.getUserMetadata().getLastRefreshTimestamp(); - assertTrue(now - tollerance <= lastRefreshTimestamp); - assertTrue(lastRefreshTimestamp <= now + tollerance); - } finally { - auth.deleteUser(newUserRecord.getUid()); + TimeUnit.SECONDS.sleep((long)Math.pow(2, i)); } + + // Ensure the lastRefreshTimestamp is approximately "now" (with a tolerance of 10 minutes). + long now = System.currentTimeMillis(); + long tolerance = TimeUnit.MINUTES.toMillis(10); + long lastRefreshTimestamp = userRecord.getUserMetadata().getLastRefreshTimestamp(); + assertTrue(now - tolerance <= lastRefreshTimestamp); + assertTrue(lastRefreshTimestamp <= now + tolerance); } @Test @@ -473,7 +493,7 @@ public void testCustomTokenWithIAM() throws Exception { if (token == null) { token = credentials.refreshAccessToken(); } - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.create(token)) .setServiceAccountId(((ServiceAccountSigner) credentials).getAccount()) .setProjectId(IntegrationTestUtils.getProjectId()) @@ -507,8 +527,8 @@ public void testVerifyIdToken() throws Exception { fail("expecting exception"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(RevocationCheckDecorator.ID_TOKEN_REVOKED_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + assertEquals(AuthErrorCode.REVOKED_ID_TOKEN, + ((FirebaseAuthException) e.getCause()).getAuthErrorCode()); } idToken = signInWithCustomToken(customToken); decoded = auth.verifyIdTokenAsync(idToken, true).get(); @@ -541,8 +561,8 @@ public void testVerifySessionCookie() throws Exception { fail("expecting exception"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(RevocationCheckDecorator.SESSION_COOKIE_REVOKED_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + assertEquals(AuthErrorCode.REVOKED_SESSION_COOKIE, + ((FirebaseAuthException) e.getCause()).getAuthErrorCode()); } idToken = signInWithCustomToken(customToken); @@ -1009,7 +1029,14 @@ private void checkRecreateUser(String uid) throws Exception { fail("No error thrown for creating user with existing ID"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals("uid-already-exists", ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals(ErrorCode.ALREADY_EXISTS, authException.getErrorCode()); + assertEquals( + "The user with the provided uid already exists (DUPLICATE_LOCAL_ID).", + authException.getMessage()); + assertNotNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.UID_ALREADY_EXISTS, authException.getAuthErrorCode()); } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index bfc8d1790..e34fede1f 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -25,10 +25,15 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.api.core.ApiFuture; import com.google.common.base.Defaults; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; + import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; @@ -53,6 +58,11 @@ public class FirebaseAuthTest { .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .build(); + private static final FirebaseAuthException testException = new FirebaseAuthException( + ErrorCode.INVALID_ARGUMENT, "Test error message", null, null, null); + private static final long VALID_SINCE = 1494364393; + private static final String TEST_USER = "testUser"; + @After public void cleanup() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -145,14 +155,14 @@ public void testProjectIdNotRequiredAtInitialization() { assertNotNull(FirebaseAuth.getInstance(app)); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = NullPointerException.class) public void testAuthExceptionNullErrorCode() { - new FirebaseAuthException(null, "test"); + new FirebaseAuthException(null, "test", null, null, null); } @Test(expected = IllegalArgumentException.class) - public void testAuthExceptionEmptyErrorCode() { - new FirebaseAuthException("", "test"); + public void testAuthExceptionNullMessage() { + new FirebaseAuthException(ErrorCode.INTERNAL, null, null, null, null); } @Test @@ -219,19 +229,48 @@ public void testVerifyIdToken() throws Exception { assertEquals("idtoken", tokenVerifier.getLastTokenString()); } + @Test + public void testVerifyIdTokenWithRevocationCheck() throws Exception { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken(VALID_SINCE + 1000)); + FirebaseAuth auth = getAuthForIdTokenVerificationWithRevocationCheck(tokenVerifier); + + FirebaseToken firebaseToken = auth.verifyIdToken("idtoken", true); + + assertEquals("testUser", firebaseToken.getUid()); + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifyIdTokenWithRevocationCheckFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken(VALID_SINCE - 1000)); + FirebaseAuth auth = getAuthForIdTokenVerificationWithRevocationCheck(tokenVerifier); + + try { + auth.verifyIdToken("idtoken", true); + fail("No error thrown for revoked ID token"); + } catch (FirebaseAuthException e) { + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertEquals("Firebase id token is revoked.", e.getMessage()); + assertNull(e.getCause()); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.REVOKED_ID_TOKEN, e.getAuthErrorCode()); + } + + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + @Test public void testVerifyIdTokenFailure() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(testException); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { auth.verifyIdToken("idtoken"); fail("No error thrown for invalid token"); } catch (FirebaseAuthException authException) { - assertEquals("TEST_CODE", authException.getErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } @@ -248,8 +287,7 @@ public void testVerifyIdTokenAsync() throws Exception { @Test public void testVerifyIdTokenAsyncFailure() throws InterruptedException { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(testException); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { @@ -257,16 +295,13 @@ public void testVerifyIdTokenAsyncFailure() throws InterruptedException { fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("TEST_CODE", authException.getErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } @Test public void testVerifyIdTokenWithCheckRevokedAsyncFailure() throws InterruptedException { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(testException); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { @@ -274,9 +309,7 @@ public void testVerifyIdTokenWithCheckRevokedAsyncFailure() throws InterruptedEx fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("TEST_CODE", authException.getErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } @@ -346,18 +379,47 @@ public void testVerifySessionCookie() throws Exception { @Test public void testVerifySessionCookieFailure() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(testException); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { auth.verifySessionCookie("idtoken"); fail("No error thrown for invalid token"); } catch (FirebaseAuthException authException) { - assertEquals("TEST_CODE", authException.getErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); + } + } + + @Test + public void testVerifySessionCookieWithRevocationCheck() throws Exception { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken(VALID_SINCE + 1000)); + FirebaseAuth auth = getAuthForSessionCookieVerificationWithRevocationCheck(tokenVerifier); + + FirebaseToken firebaseToken = auth.verifySessionCookie("cookie", true); + + assertEquals("testUser", firebaseToken.getUid()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifySessionCookieWithRevocationCheckFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken(VALID_SINCE - 1000)); + FirebaseAuth auth = getAuthForSessionCookieVerificationWithRevocationCheck(tokenVerifier); + + try { + auth.verifySessionCookie("cookie", true); + fail("No error thrown for revoked session cookie"); + } catch (FirebaseAuthException e) { + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertEquals("Firebase session cookie is revoked.", e.getMessage()); + assertNull(e.getCause()); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.REVOKED_SESSION_COOKIE, e.getAuthErrorCode()); } + + assertEquals("cookie", tokenVerifier.getLastTokenString()); } @Test @@ -373,8 +435,7 @@ public void testVerifySessionCookieAsync() throws Exception { @Test public void testVerifySessionCookieAsyncFailure() throws InterruptedException { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(testException); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { @@ -382,16 +443,13 @@ public void testVerifySessionCookieAsyncFailure() throws InterruptedException { fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("TEST_CODE", authException.getErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } @Test public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws InterruptedException { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(testException); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { @@ -399,12 +457,24 @@ public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws Interru fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("TEST_CODE", authException.getErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } + private FirebaseToken getFirebaseToken(String subject) { + return new FirebaseToken(ImmutableMap.of("sub", subject)); + } + + private FirebaseToken getFirebaseToken(long issuedAt) { + return new FirebaseToken(ImmutableMap.of("sub", TEST_USER, "iat", issuedAt)); + } + + FirebaseAuth getAuthForIdTokenVerificationWithRevocationCheck( + FirebaseTokenVerifier tokenVerifier) { + FirebaseApp app = getFirebaseAppForUserRetrieval(); + return getAuthForIdTokenVerification(app, Suppliers.ofInstance(tokenVerifier)); + } + private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVerifier) { return getAuthForIdTokenVerification(Suppliers.ofInstance(tokenVerifier)); } @@ -412,7 +482,13 @@ private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVe private FirebaseAuth getAuthForIdTokenVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); - FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); + return getAuthForIdTokenVerification(app, tokenVerifierSupplier); + } + + private FirebaseAuth getAuthForIdTokenVerification( + FirebaseApp app, + Supplier tokenVerifierSupplier) { + FirebaseUserManager userManager = FirebaseUserManager.createUserManager(app, null); return FirebaseAuth.builder() .setFirebaseApp(app) .setIdTokenVerifier(tokenVerifierSupplier) @@ -420,6 +496,12 @@ private FirebaseAuth getAuthForIdTokenVerification( .build(); } + FirebaseAuth getAuthForSessionCookieVerificationWithRevocationCheck( + FirebaseTokenVerifier tokenVerifier) { + FirebaseApp app = getFirebaseAppForUserRetrieval(); + return getAuthForSessionCookieVerification(app, Suppliers.ofInstance(tokenVerifier)); + } + private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) { return getAuthForSessionCookieVerification(Suppliers.ofInstance(tokenVerifier)); } @@ -427,7 +509,13 @@ private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier t private FirebaseAuth getAuthForSessionCookieVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); - FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); + return getAuthForSessionCookieVerification(app, tokenVerifierSupplier); + } + + private FirebaseAuth getAuthForSessionCookieVerification( + FirebaseApp app, + Supplier tokenVerifierSupplier) { + FirebaseUserManager userManager = FirebaseUserManager.createUserManager(app, null); return FirebaseAuth.builder() .setFirebaseApp(app) .setCookieVerifier(tokenVerifierSupplier) @@ -435,13 +523,22 @@ private FirebaseAuth getAuthForSessionCookieVerification( .build(); } + private FirebaseApp getFirebaseAppForUserRetrieval() { + String getUserResponse = TestUtils.loadResource("getUser.json"); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent(getUserResponse)) + .build(); + return FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setHttpTransport(transport) + .setProjectId("test-project-id") + .build()); + } + public static TestResponseInterceptor setUserManager( AbstractFirebaseAuth.Builder builder, FirebaseApp app, String tenantId) { TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseUserManager userManager = FirebaseUserManager.builder() - .setFirebaseApp(app) - .setTenantId(tenantId) - .build(); + FirebaseUserManager userManager = FirebaseUserManager.createUserManager(app, tenantId); userManager.setInterceptor(interceptor); builder.setUserManager(Suppliers.ofInstance(userManager)); return interceptor; diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java index 379da0a57..833e2ed95 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -17,6 +17,7 @@ package com.google.firebase.auth; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import com.google.api.client.auth.openidconnect.IdTokenVerifier; @@ -30,15 +31,15 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.testing.ServiceAccount; import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.concurrent.TimeUnit; import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; public class FirebaseTokenVerifierImplTest { @@ -55,9 +56,6 @@ public class FirebaseTokenVerifierImplTest { private static final String TEST_TOKEN_ISSUER = "https://test.token.issuer"; - @Rule - public ExpectedException thrown = ExpectedException.none(); - private FirebaseTokenVerifier tokenVerifier; private TestTokenFactory tokenFactory; @@ -80,108 +78,188 @@ public void testVerifyToken() throws Exception { } @Test - public void testVerifyTokenWithoutKeyId() throws Exception { + public void testVerifyTokenWithoutKeyId() { String token = createTokenWithoutKeyId(); - thrown.expectMessage("Firebase test token has no \"kid\" claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has no \"kid\" claim. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenFirebaseCustomToken() throws Exception { + public void testVerifyTokenFirebaseCustomToken() { String token = createCustomToken(); - thrown.expectMessage("verifyTestToken() expects a test token, but was given a custom token."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "verifyTestToken() expects a test token, but was given a custom token. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenIncorrectAlgorithm() throws Exception { + public void testVerifyTokenIncorrectAlgorithm() { String token = createTokenWithIncorrectAlgorithm(); - thrown.expectMessage("Firebase test token has incorrect algorithm."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has incorrect algorithm. " + + "Expected \"RS256\" but got \"HSA\". " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenIncorrectAudience() throws Exception { + public void testVerifyTokenIncorrectAudience() { String token = createTokenWithIncorrectAudience(); - thrown.expectMessage("Firebase test token has incorrect \"aud\" (audience) claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has incorrect \"aud\" (audience) claim. " + + "Expected \"proj-test-101\" but got \"invalid-audience\". " + + "Make sure the test token comes from the same Firebase project as the service account " + + "used to authenticate this SDK. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenIncorrectIssuer() throws Exception { + public void testVerifyTokenIncorrectIssuer() { String token = createTokenWithIncorrectIssuer(); - thrown.expectMessage("Firebase test token has incorrect \"iss\" (issuer) claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has incorrect \"iss\" (issuer) claim. " + + "Expected \"https://test.token.issuer\" but got " + + "\"https://incorrect.issuer.prefix/proj-test-101\". Make sure the test token comes " + + "from the same Firebase project as the service account used to authenticate this SDK. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenMissingSubject() throws Exception { + public void testVerifyTokenMissingSubject() { String token = createTokenWithSubject(null); - thrown.expectMessage("Firebase test token has no \"sub\" (subject) claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has no \"sub\" (subject) claim. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenEmptySubject() throws Exception { + public void testVerifyTokenEmptySubject() { String token = createTokenWithSubject(""); - thrown.expectMessage("Firebase test token has an empty string \"sub\" (subject) claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has an empty string \"sub\" (subject) claim. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenLongSubject() throws Exception { + public void testVerifyTokenLongSubject() { String token = createTokenWithSubject(Strings.repeat("a", 129)); - thrown.expectMessage( - "Firebase test token has \"sub\" (subject) claim longer than 128 characters."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has \"sub\" (subject) claim longer " + + "than 128 characters. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenIssuedAtInFuture() throws Exception { + public void testVerifyTokenIssuedAtInFuture() { long tenMinutesIntoTheFuture = (TestTokenFactory.CLOCK.currentTimeMillis() / 1000) + TimeUnit.MINUTES.toSeconds(10); String token = createTokenWithTimestamps( tenMinutesIntoTheFuture, tenMinutesIntoTheFuture + TimeUnit.HOURS.toSeconds(1)); - thrown.expectMessage("Firebase test token has expired or is not yet valid."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token is not yet valid. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenExpired() throws Exception { + public void testVerifyTokenExpired() { long twoHoursInPast = (TestTokenFactory.CLOCK.currentTimeMillis() / 1000) - TimeUnit.HOURS.toSeconds(2); String token = createTokenWithTimestamps( twoHoursInPast, twoHoursInPast + TimeUnit.HOURS.toSeconds(1)); - thrown.expectMessage("Firebase test token has expired or is not yet valid."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has expired. " + + "Get a fresh test token and try again. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkException(e, message, AuthErrorCode.EXPIRED_ID_TOKEN); + } } @Test - public void testVerifyTokenIncorrectCert() throws Exception { + public void testVerifyTokenSignatureMismatch() { String token = tokenFactory.createToken(); GooglePublicKeysManager publicKeysManager = newPublicKeysManager( ServiceAccount.NONE.getCert()); FirebaseTokenVerifier tokenVerifier = newTestTokenVerifier(publicKeysManager); - thrown.expectMessage("Failed to verify the signature of Firebase test token. " - + "See https://test.doc.url for details on how to retrieve a test token."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Failed to verify the signature of Firebase test token. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void verifyTokenCertificateError() { + public void testMalformedCert() { + String token = tokenFactory.createToken(); + GooglePublicKeysManager publicKeysManager = newPublicKeysManager("malformed.cert"); + FirebaseTokenVerifier tokenVerifier = newTestTokenVerifier(publicKeysManager); + + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Error while fetching public key certificates: Could not parse certificate"; + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertTrue(e.getMessage().startsWith(message)); + assertTrue(e.getCause() instanceof GeneralSecurityException); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CERTIFICATE_FETCH_FAILED, e.getAuthErrorCode()); + } + } + + @Test + public void testCertificateFetchError() { MockHttpTransport failingTransport = new MockHttpTransport() { @Override public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { @@ -195,26 +273,57 @@ public LowLevelHttpRequest buildRequest(String method, String url) throws IOExce try { idTokenVerifier.verifyToken(token); Assert.fail("No exception thrown"); - } catch (FirebaseAuthException expected) { - assertTrue(expected.getCause() instanceof IOException); - assertEquals("Expected error", expected.getCause().getMessage()); + } catch (FirebaseAuthException e) { + String message = "Error while fetching public key certificates: Expected error"; + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals(message, e.getMessage()); + assertTrue(e.getCause() instanceof IOException); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CERTIFICATE_FETCH_FAILED, e.getAuthErrorCode()); } } @Test - public void testLegacyCustomToken() throws Exception { - thrown.expectMessage( - "verifyTestToken() expects a test token, but was given a legacy custom token."); - tokenVerifier.verifyToken(LEGACY_CUSTOM_TOKEN); + public void testMalformedSignature() { + String token = tokenFactory.createToken(); + String[] segments = token.split("\\."); + token = String.format("%s.%s.%s", segments[0], segments[1], "MalformedSignature"); + + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Failed to verify the signature of Firebase test token. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testMalformedToken() throws Exception { - thrown.expectMessage( - "Failed to parse Firebase test token. Make sure you passed a string that represents a " - + "complete and valid JWT. See https://test.doc.url for details on how to retrieve " - + "a test token."); - tokenVerifier.verifyToken("not.a.jwt"); + public void testLegacyCustomToken() { + try { + tokenVerifier.verifyToken(LEGACY_CUSTOM_TOKEN); + } catch (FirebaseAuthException e) { + String message = "verifyTestToken() expects a test token, but was given a " + + "legacy custom token. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } + } + + @Test + public void testMalformedToken() { + try { + tokenVerifier.verifyToken("not.a.jwt"); + } catch (FirebaseAuthException e) { + String message = "Failed to parse Firebase test token. " + + "Make sure you passed a string that represents a complete and valid JWT. " + + "See https://test.doc.url for details on how to retrieve a test token."; + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertEquals(message, e.getMessage()); + assertTrue(e.getCause() instanceof IllegalArgumentException); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.INVALID_ID_TOKEN, e.getAuthErrorCode()); + } } @Test @@ -238,7 +347,7 @@ public void testVerifyTokenDifferentTenantIds() { .build() .verifyToken(createTokenWithTenantId("TENANT_2")); } catch (FirebaseAuthException e) { - assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode()); assertEquals( "The tenant ID ('TENANT_2') of the token did not match the expected value ('TENANT_1')", e.getMessage()); @@ -253,7 +362,7 @@ public void testVerifyTokenMissingTenantId() { .build() .verifyToken(tokenFactory.createToken()); } catch (FirebaseAuthException e) { - assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode()); assertEquals( "The tenant ID ('') of the token did not match the expected value ('TENANT_ID')", e.getMessage()); @@ -267,7 +376,7 @@ public void testVerifyTokenUnexpectedTenantId() { .build() .verifyToken(createTokenWithTenantId("TENANT_ID")); } catch (FirebaseAuthException e) { - assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode()); assertEquals( "The tenant ID ('TENANT_ID') of the token did not match the expected value ('')", e.getMessage()); @@ -323,13 +432,8 @@ private GooglePublicKeysManager newPublicKeysManager(HttpTransport transport) { } private FirebaseTokenVerifier newTestTokenVerifier(GooglePublicKeysManager publicKeysManager) { - return FirebaseTokenVerifierImpl.builder() - .setShortName("test token") - .setMethod("verifyTestToken()") - .setDocUrl("https://test.doc.url") - .setJsonFactory(TestTokenFactory.JSON_FACTORY) + return fullyPopulatedBuilder() .setPublicKeysManager(publicKeysManager) - .setIdTokenVerifier(newIdTokenVerifier()) .build(); } @@ -340,6 +444,8 @@ private FirebaseTokenVerifierImpl.Builder fullyPopulatedBuilder() { .setDocUrl("https://test.doc.url") .setJsonFactory(TestTokenFactory.JSON_FACTORY) .setPublicKeysManager(newPublicKeysManager(ServiceAccount.EDITOR.getCert())) + .setInvalidTokenErrorCode(AuthErrorCode.INVALID_ID_TOKEN) + .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_ID_TOKEN) .setIdTokenVerifier(newIdTokenVerifier()); } @@ -396,6 +502,18 @@ private String createTokenWithTimestamps(long issuedAtSeconds, long expirationSe return tokenFactory.createToken(payload); } + private void checkInvalidTokenException(FirebaseAuthException e, String message) { + checkException(e, message, AuthErrorCode.INVALID_ID_TOKEN); + } + + private void checkException(FirebaseAuthException e, String message, AuthErrorCode errorCode) { + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertEquals(message, e.getMessage()); + assertNull(e.getCause()); + assertNull(e.getHttpResponse()); + assertEquals(errorCode, e.getAuthErrorCode()); + } + private String createTokenWithTenantId(String tenantId) { Payload payload = tokenFactory.createTokenPayload(); payload.set("firebase", ImmutableMap.of("tenant", tenantId)); diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 7012ef6bf..772f9ba8d 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -40,18 +40,17 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; -import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.auth.multitenancy.TenantAwareFirebaseAuth; import com.google.firebase.auth.multitenancy.TenantManager; import com.google.firebase.internal.SdkUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; import com.google.firebase.testing.TestResponseInterceptor; import com.google.firebase.testing.TestUtils; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigDecimal; @@ -60,7 +59,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; - import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -92,6 +90,10 @@ public class FirebaseUserManagerTest { private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants"; + private static final String SAML_RESPONSE = TestUtils.loadResource("saml.json"); + + private static final String OIDC_RESPONSE = TestUtils.loadResource("oidc.json"); + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -99,7 +101,7 @@ public void tearDown() { @Test public void testProjectIdRequired() { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .build()); FirebaseAuth auth = FirebaseAuth.getInstance(); @@ -133,7 +135,12 @@ public void testGetUserWithNotFoundError() throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals( + "No user record found for the provided user ID: testuser", authException.getMessage()); + assertNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -156,7 +163,13 @@ public void testGetUserByEmailWithNotFoundError() throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals( + "No user record found for the provided email: testuser@example.com", + authException.getMessage()); + assertNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -179,13 +192,19 @@ public void testGetUserByPhoneNumberWithNotFoundError() throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals( + "No user record found for the provided phone number: +1234567890", + authException.getMessage()); + assertNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @Test public void testGetUsersExceeds100() throws Exception { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .build()); List identifiers = new ArrayList<>(); @@ -203,7 +222,7 @@ public void testGetUsersExceeds100() throws Exception { @Test public void testGetUsersNull() throws Exception { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .build()); try { @@ -262,7 +281,7 @@ public void testGetUsersMultipleIdentifierTypes() throws Exception { ).replace("'", "\"")); UidIdentifier doesntExist = new UidIdentifier("this-uid-doesnt-exist"); - List ids = ImmutableList.of( + List ids = ImmutableList.of( new UidIdentifier("uid1"), new EmailIdentifier("user2@example.com"), new PhoneIdentifier("+15555550003"), @@ -284,7 +303,7 @@ private Collection userRecordsToUids(Collection userRecords) } @Test - public void testInvalidUidIdentifier() throws Exception { + public void testInvalidUidIdentifier() { try { new UidIdentifier("too long " + Strings.repeat(".", 128)); fail("No error thrown for invalid uid"); @@ -294,7 +313,7 @@ public void testInvalidUidIdentifier() throws Exception { } @Test - public void testInvalidEmailIdentifier() throws Exception { + public void testInvalidEmailIdentifier() { try { new EmailIdentifier("invalid email addr"); fail("No error thrown for invalid email"); @@ -304,7 +323,7 @@ public void testInvalidEmailIdentifier() throws Exception { } @Test - public void testInvalidPhoneIdentifier() throws Exception { + public void testInvalidPhoneIdentifier() { try { new PhoneIdentifier("invalid phone number"); fail("No error thrown for invalid phone number"); @@ -314,7 +333,7 @@ public void testInvalidPhoneIdentifier() throws Exception { } @Test - public void testInvalidProviderIdentifier() throws Exception { + public void testInvalidProviderIdentifier() { try { new ProviderIdentifier("", "valid-uid"); fail("No error thrown for invalid provider id"); @@ -437,8 +456,8 @@ public void testDeleteUser() throws Exception { } @Test - public void testDeleteUsersExceeds1000() throws Exception { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + public void testDeleteUsersExceeds1000() { + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .build()); List ids = new ArrayList<>(); @@ -454,8 +473,8 @@ public void testDeleteUsersExceeds1000() throws Exception { } @Test - public void testDeleteUsersInvalidId() throws Exception { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + public void testDeleteUsersInvalidId() { + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .build()); try { @@ -773,9 +792,15 @@ public void call(FirebaseAuth auth) throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); FirebaseAuth auth = getRetryDisabledAuth(response); + Map codes = ImmutableMap.of( + 302, ErrorCode.UNKNOWN, + 400, ErrorCode.INVALID_ARGUMENT, + 401, ErrorCode.UNAUTHENTICATED, + 404, ErrorCode.NOT_FOUND, + 500, ErrorCode.INTERNAL); // Test for common HTTP error codes - for (int code : ImmutableList.of(302, 400, 401, 404, 500)) { + for (int code : codes.keySet()) { for (UserManagerOp operation : operations) { // Need to reset these every iteration response.setContent("{}"); @@ -786,15 +811,17 @@ public void call(FirebaseAuth auth) throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - String msg = String.format("Unexpected HTTP response with status: %d; body: {}", code); + assertEquals(codes.get(code), authException.getErrorCode()); + String msg = String.format("Unexpected HTTP response with status: %d\n{}", code); assertEquals(msg, authException.getMessage()); - assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); - assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); + assertTrue(authException.getCause() instanceof HttpResponseException); + assertNotNull(authException.getHttpResponse()); + assertNull(authException.getAuthErrorCode()); } } } - // Test error payload parsing + // Test error payload with code for (UserManagerOp operation : operations) { response.setContent("{\"error\": {\"message\": \"USER_NOT_FOUND\"}}"); response.setStatusCode(500); @@ -804,9 +831,33 @@ public void call(FirebaseAuth auth) throws Exception { } catch (ExecutionException e) { assertThat(e.getCause().toString(), e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("Firebase Auth service responded with an error", authException.getMessage()); - assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals( + "No user record found for the given identifier (USER_NOT_FOUND).", + authException.getMessage()); + assertTrue(authException.getCause() instanceof HttpResponseException); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); + } + } + + // Test error payload with code and details + for (UserManagerOp operation : operations) { + response.setContent("{\"error\": {\"message\": \"USER_NOT_FOUND: Extra details\"}}"); + response.setStatusCode(500); + try { + operation.call(auth); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause().toString(), e.getCause() instanceof FirebaseAuthException); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals( + "No user record found for the given identifier (USER_NOT_FOUND): Extra details", + authException.getMessage()); + assertTrue(authException.getCause() instanceof HttpResponseException); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } } @@ -820,8 +871,12 @@ public void testGetUserMalformedJsonError() throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertThat(authException.getCause(), instanceOf(IOException.class)); - assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); + assertEquals(ErrorCode.UNKNOWN, authException.getErrorCode()); + assertTrue( + authException.getMessage().startsWith("Error while parsing HTTP response: ")); + assertTrue(authException.getCause() instanceof IOException); + assertNotNull(authException.getHttpResponse()); + assertNull(authException.getAuthErrorCode()); } } @@ -837,10 +892,12 @@ public void testGetUserUnexpectedHttpError() throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); - assertEquals("Unexpected HTTP response with status: 500; body: {\"not\" json}", + assertEquals(ErrorCode.INTERNAL, authException.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n{\"not\" json}", authException.getMessage()); - assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); + assertTrue(authException.getCause() instanceof HttpResponseException); + assertNotNull(authException.getHttpResponse()); + assertNull(authException.getAuthErrorCode()); } } @@ -848,7 +905,7 @@ public void testGetUserUnexpectedHttpError() throws Exception { public void testTimeout() throws Exception { MockHttpTransport transport = new MultiRequestMockHttpTransport(ImmutableList.of( new MockLowLevelHttpResponse().setContent(TestUtils.loadResource("getUser.json")))); - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .setProjectId("test-project-id") .setHttpTransport(transport) @@ -1375,8 +1432,33 @@ public void testHttpErrorWithCode() { 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()); - assertThat(e.getCause(), instanceOf(HttpResponseException.class)); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertEquals( + "The domain of the continue URL is not whitelisted (UNAUTHORIZED_DOMAIN).", + e.getMessage()); + assertEquals(AuthErrorCode.UNAUTHORIZED_CONTINUE_URL, e.getAuthErrorCode()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void testHttpErrorWithUnknownCode() { + String content = "{\"error\": {\"message\": \"SOMETHING_NEW\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(content) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); + 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(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n" + content, e.getMessage()); + assertNull(e.getAuthErrorCode()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); } } @@ -1391,15 +1473,17 @@ public void testUnexpectedHttpError() { userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null); fail("No exception thrown for HTTP error"); } catch (FirebaseAuthException e) { - assertEquals("internal-error", e.getErrorCode()); - assertThat(e.getCause(), instanceOf(HttpResponseException.class)); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n{}", e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } } @Test public void testCreateOidcProvider() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() .setProviderId("oidc.provider-id") @@ -1424,8 +1508,7 @@ public void testCreateOidcProvider() throws Exception { @Test public void testCreateOidcProviderAsync() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() .setProviderId("oidc.provider-id") @@ -1451,8 +1534,7 @@ public void testCreateOidcProviderAsync() throws Exception { @Test public void testCreateOidcProviderMinimal() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); // Only the 'enabled' and 'displayName' fields can be omitted from an OIDC provider config // creation request. OidcProviderConfig.CreateRequest createRequest = @@ -1474,24 +1556,32 @@ public void testCreateOidcProviderMinimal() throws Exception { } @Test - public void testCreateOidcProviderError() throws Exception { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + public void testCreateOidcProviderError() { + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(message) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest().setProviderId("oidc.provider-id"); + try { - FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + auth.createOidcProviderConfig(createRequest); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "Unexpected HTTP response with status: 500\n" + message, + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); } @Test public void testCreateOidcProviderMissingId() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() .setDisplayName("DISPLAY_NAME") @@ -1509,8 +1599,7 @@ public void testCreateOidcProviderMissingId() throws Exception { @Test public void testTenantAwareCreateOidcProvider() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( - "TENANT_ID", - TestUtils.loadResource("oidc.json")); + "TENANT_ID", OIDC_RESPONSE); OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() .setProviderId("oidc.provider-id") @@ -1529,8 +1618,7 @@ public void testTenantAwareCreateOidcProvider() throws Exception { @Test public void testUpdateOidcProvider() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig.UpdateRequest request = new OidcProviderConfig.UpdateRequest("oidc.provider-id") .setDisplayName("DISPLAY_NAME") @@ -1554,8 +1642,7 @@ public void testUpdateOidcProvider() throws Exception { @Test public void testUpdateOidcProviderAsync() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig.UpdateRequest request = new OidcProviderConfig.UpdateRequest("oidc.provider-id") .setDisplayName("DISPLAY_NAME") @@ -1580,8 +1667,7 @@ public void testUpdateOidcProviderAsync() throws Exception { @Test public void testUpdateOidcProviderMinimal() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig.UpdateRequest request = new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME"); @@ -1599,7 +1685,7 @@ public void testUpdateOidcProviderMinimal() throws Exception { @Test public void testUpdateOidcProviderConfigNoValues() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + initializeAppForUserManagement(OIDC_RESPONSE); try { FirebaseAuth.getInstance().updateOidcProviderConfig( new OidcProviderConfig.UpdateRequest("oidc.provider-id")); @@ -1611,25 +1697,32 @@ public void testUpdateOidcProviderConfigNoValues() throws Exception { @Test public void testUpdateOidcProviderConfigError() { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(message) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); OidcProviderConfig.UpdateRequest request = new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME"); + try { - FirebaseAuth.getInstance().updateOidcProviderConfig(request); + auth.updateOidcProviderConfig(request); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "Unexpected HTTP response with status: 500\n" + message, + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); } @Test public void testTenantAwareUpdateOidcProvider() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( - "TENANT_ID", - TestUtils.loadResource("oidc.json")); + "TENANT_ID", OIDC_RESPONSE); TenantAwareFirebaseAuth tenantAwareAuth = FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); OidcProviderConfig.UpdateRequest request = @@ -1656,8 +1749,7 @@ public void testTenantAwareUpdateOidcProvider() throws Exception { @Test public void testGetOidcProviderConfig() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig config = FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id"); @@ -1669,8 +1761,7 @@ public void testGetOidcProviderConfig() throws Exception { @Test public void testGetOidcProviderConfigAsync() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig config = FirebaseAuth.getInstance().getOidcProviderConfigAsync("oidc.provider-id").get(); @@ -1682,7 +1773,7 @@ public void testGetOidcProviderConfigAsync() throws Exception { @Test public void testGetOidcProviderConfigMissingId() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + initializeAppForUserManagement(OIDC_RESPONSE); try { FirebaseAuth.getInstance().getOidcProviderConfig(null); @@ -1694,7 +1785,7 @@ public void testGetOidcProviderConfigMissingId() throws Exception { @Test public void testGetOidcProviderConfigInvalidId() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + initializeAppForUserManagement(OIDC_RESPONSE); try { FirebaseAuth.getInstance().getOidcProviderConfig("saml.invalid-oidc-provider-id"); @@ -1705,7 +1796,7 @@ public void testGetOidcProviderConfigInvalidId() throws Exception { } @Test - public void testGetOidcProviderConfigWithNotFoundError() throws Exception { + public void testGetOidcProviderConfigWithNotFoundError() { TestResponseInterceptor interceptor = initializeAppForUserManagementWithStatusCode(404, "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); @@ -1713,7 +1804,14 @@ public void testGetOidcProviderConfigWithNotFoundError() throws Exception { FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals( + "No IdP configuration found corresponding to the provided identifier " + + "(CONFIGURATION_NOT_FOUND).", + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, e.getAuthErrorCode()); } checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); } @@ -1721,8 +1819,7 @@ public void testGetOidcProviderConfigWithNotFoundError() throws Exception { @Test public void testGetTenantAwareOidcProviderConfig() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( - "TENANT_ID", - TestUtils.loadResource("oidc.json")); + "TENANT_ID", OIDC_RESPONSE); TenantAwareFirebaseAuth tenantAwareAuth = FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); @@ -1772,18 +1869,25 @@ public void testListOidcProviderConfigsAsync() throws Exception { } @Test - public void testListOidcProviderConfigsError() throws Exception { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + public void testListOidcProviderConfigsError() { + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(message) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); try { - FirebaseAuth.getInstance().listOidcProviderConfigs(null, 99); + auth.listOidcProviderConfigs(null, 99); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "Unexpected HTTP response with status: 500\n" + message, + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); } @Test @@ -1890,7 +1994,14 @@ public void testDeleteOidcProviderConfigWithNotFoundError() { FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.UNKNOWN"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals( + "No IdP configuration found corresponding to the provided identifier " + + "(CONFIGURATION_NOT_FOUND).", + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, e.getAuthErrorCode()); } checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.UNKNOWN"); } @@ -1912,8 +2023,7 @@ public void testTenantAwareDeleteOidcProviderConfig() throws Exception { @Test public void testCreateSamlProvider() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig.CreateRequest createRequest = new SamlProviderConfig.CreateRequest() .setProviderId("saml.provider-id") @@ -1958,8 +2068,7 @@ public void testCreateSamlProvider() throws Exception { @Test public void testCreateSamlProviderAsync() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig.CreateRequest createRequest = new SamlProviderConfig.CreateRequest() .setProviderId("saml.provider-id") @@ -2005,8 +2114,7 @@ public void testCreateSamlProviderAsync() throws Exception { @Test public void testCreateSamlProviderMinimal() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); // Only the 'enabled', 'displayName', and 'signRequest' fields can be omitted from a SAML // provider config creation request. SamlProviderConfig.CreateRequest createRequest = @@ -2046,23 +2154,31 @@ public void testCreateSamlProviderMinimal() throws Exception { @Test public void testCreateSamlProviderError() { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(message) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); SamlProviderConfig.CreateRequest createRequest = new SamlProviderConfig.CreateRequest().setProviderId("saml.provider-id"); + try { - FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + auth.createSamlProviderConfig(createRequest); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "Unexpected HTTP response with status: 500\n" + message, + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); } @Test public void testCreateSamlProviderMissingId() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig.CreateRequest createRequest = new SamlProviderConfig.CreateRequest() .setDisplayName("DISPLAY_NAME") @@ -2084,8 +2200,7 @@ public void testCreateSamlProviderMissingId() throws Exception { @Test public void testTenantAwareCreateSamlProvider() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( - "TENANT_ID", - TestUtils.loadResource("saml.json")); + "TENANT_ID", SAML_RESPONSE); SamlProviderConfig.CreateRequest createRequest = new SamlProviderConfig.CreateRequest() .setProviderId("saml.provider-id") @@ -2108,8 +2223,7 @@ public void testTenantAwareCreateSamlProvider() throws Exception { @Test public void testUpdateSamlProvider() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig.UpdateRequest updateRequest = new SamlProviderConfig.UpdateRequest("saml.provider-id") .setDisplayName("DISPLAY_NAME") @@ -2156,8 +2270,7 @@ public void testUpdateSamlProvider() throws Exception { @Test public void testUpdateSamlProviderAsync() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig.UpdateRequest updateRequest = new SamlProviderConfig.UpdateRequest("saml.provider-id") .setDisplayName("DISPLAY_NAME") @@ -2205,8 +2318,7 @@ public void testUpdateSamlProviderAsync() throws Exception { @Test public void testUpdateSamlProviderMinimal() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig.UpdateRequest request = new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME"); @@ -2224,7 +2336,7 @@ public void testUpdateSamlProviderMinimal() throws Exception { @Test public void testUpdateSamlProviderConfigNoValues() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + initializeAppForUserManagement(SAML_RESPONSE); try { FirebaseAuth.getInstance().updateSamlProviderConfig( new SamlProviderConfig.UpdateRequest("saml.provider-id")); @@ -2235,26 +2347,33 @@ public void testUpdateSamlProviderConfigNoValues() throws Exception { } @Test - public void testUpdateSamlProviderConfigError() throws Exception { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + public void testUpdateSamlProviderConfigError() { + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(message) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); SamlProviderConfig.UpdateRequest request = new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME"); + try { - FirebaseAuth.getInstance().updateSamlProviderConfig(request); + auth.updateSamlProviderConfig(request); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "Unexpected HTTP response with status: 500\n" + message, + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); } @Test public void testTenantAwareUpdateSamlProvider() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( - "TENANT_ID", - TestUtils.loadResource("saml.json")); + "TENANT_ID", SAML_RESPONSE); TenantAwareFirebaseAuth tenantAwareAuth = FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); SamlProviderConfig.UpdateRequest updateRequest = @@ -2286,8 +2405,7 @@ public void testTenantAwareUpdateSamlProvider() throws Exception { @Test public void testGetSamlProviderConfig() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig config = FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id"); @@ -2299,8 +2417,7 @@ public void testGetSamlProviderConfig() throws Exception { @Test public void testGetSamlProviderConfigAsync() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig config = FirebaseAuth.getInstance().getSamlProviderConfigAsync("saml.provider-id").get(); @@ -2324,7 +2441,7 @@ public void testGetSamlProviderConfigMissingId() throws Exception { @Test public void testGetSamlProviderConfigInvalidId() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + initializeAppForUserManagement(SAML_RESPONSE); try { FirebaseAuth.getInstance().getSamlProviderConfig("oidc.invalid-saml-provider-id"); @@ -2343,7 +2460,14 @@ public void testGetSamlProviderConfigWithNotFoundError() { FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals( + "No IdP configuration found corresponding to the provided identifier " + + "(CONFIGURATION_NOT_FOUND).", + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, e.getAuthErrorCode()); } checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); } @@ -2351,8 +2475,7 @@ public void testGetSamlProviderConfigWithNotFoundError() { @Test public void testGetTenantAwareSamlProviderConfig() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( - "TENANT_ID", - TestUtils.loadResource("saml.json")); + "TENANT_ID", SAML_RESPONSE); TenantAwareFirebaseAuth tenantAwareAuth = FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); @@ -2403,18 +2526,25 @@ public void testListSamlProviderConfigsAsync() throws Exception { } @Test - public void testListSamlProviderConfigsError() throws Exception { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + public void testListSamlProviderConfigsError() { + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(message) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); try { - FirebaseAuth.getInstance().listSamlProviderConfigs(null, 99); + auth.listSamlProviderConfigs(null, 99); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "Unexpected HTTP response with status: 500\n" + message, + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); } @Test @@ -2521,7 +2651,14 @@ public void testDeleteSamlProviderConfigWithNotFoundError() { FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.UNKNOWN"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals( + "No IdP configuration found corresponding to the provided identifier " + + "(CONFIGURATION_NOT_FOUND).", + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, e.getAuthErrorCode()); } checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.UNKNOWN"); } @@ -2543,7 +2680,7 @@ public void testTenantAwareDeleteSamlProviderConfig() throws Exception { private static TestResponseInterceptor initializeAppForUserManagementWithStatusCode( int statusCode, String response) { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .setHttpTransport( new MockHttpTransport.Builder().setLowLevelHttpResponse( @@ -2579,7 +2716,7 @@ private static void initializeAppWithResponses(String... responses) { mocks.add(new MockLowLevelHttpResponse().setContent(response)); } MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks); - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .setHttpTransport(transport) .setProjectId("test-project-id") @@ -2597,9 +2734,8 @@ private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse respon final MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(response) .build(); - final FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + final FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) - .setProjectId("test-project-id") .setHttpTransport(transport) .build()); return FirebaseAuth.builder() @@ -2608,9 +2744,10 @@ private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse respon @Override public FirebaseUserManager get() { return FirebaseUserManager.builder() - .setFirebaseApp(app) - .setHttpRequestFactory(transport.createRequestFactory()) - .build(); + .setProjectId("test-project-id") + .setHttpRequestFactory(transport.createRequestFactory()) + .setJsonFactory(Utils.getDefaultJsonFactory()) + .build(); } }) .build(); @@ -2680,13 +2817,7 @@ private static void checkRequestHeaders(TestResponseInterceptor interceptor) { private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) { HttpRequest request = interceptor.getResponse().getRequest(); - if (method.equals("PATCH")) { - assertEquals("PATCH", - request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override")); - assertEquals("POST", request.getRequestMethod()); - } else { - assertEquals(method, request.getRequestMethod()); - } + assertEquals(method, request.getRequestMethod()); assertEquals(url, request.getUrl().toString().split("\\?")[0]); } diff --git a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java index c01ac6501..e027882c0 100644 --- a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java +++ b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java @@ -21,7 +21,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.ErrorCode; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; @@ -36,8 +36,9 @@ public static void assertOidcProviderConfigDoesNotExist( fail("No error thrown for getting a deleted OIDC provider config."); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -48,8 +49,9 @@ public static void assertSamlProviderConfigDoesNotExist( fail("No error thrown for getting a deleted SAML provider config."); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, authException.getAuthErrorCode()); } } diff --git a/src/test/java/com/google/firebase/auth/UserTestUtils.java b/src/test/java/com/google/firebase/auth/UserTestUtils.java index aa86e6e19..8d6f7fc32 100644 --- a/src/test/java/com/google/firebase/auth/UserTestUtils.java +++ b/src/test/java/com/google/firebase/auth/UserTestUtils.java @@ -20,7 +20,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.ErrorCode; import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -37,7 +37,7 @@ public static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, Str fail("No error thrown for getting a user which was expected to be absent."); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, + assertEquals(ErrorCode.NOT_FOUND, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } diff --git a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java index bc98add3c..32f9fd543 100644 --- a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java +++ b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java @@ -18,10 +18,14 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpStatusCodes; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.auth.ServiceAccountSigner; @@ -29,21 +33,22 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.io.BaseEncoding; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseAuthException; 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 { + public void testServiceAccountCryptoSigner() throws Exception { ServiceAccountCredentials credentials = ServiceAccountCredentials.fromStream( ServiceAccount.EDITOR.asStream()); byte[] expected = credentials.sign("foo".getBytes()); @@ -63,7 +68,7 @@ public void testInvalidServiceAccountCryptoSigner() { } @Test - public void testIAMCryptoSigner() throws IOException { + public void testIAMCryptoSigner() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( ImmutableMap.of("signature", signature)); @@ -84,6 +89,29 @@ public void testIAMCryptoSigner() throws IOException { assertEquals(url, interceptor.getResponse().getRequest().getUrl().toString()); } + @Test + public void testIAMCryptoSignerHttpError() { + String error = "{\"error\": {\"status\":\"INTERNAL\", \"message\": \"Test error\"}}"; + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(error)) + .build(); + CryptoSigners.IAMCryptoSigner signer = new CryptoSigners.IAMCryptoSigner( + transport.createRequestFactory(), + Utils.getDefaultJsonFactory(), + "test-service-account@iam.gserviceaccount.com"); + try { + signer.sign("foo".getBytes()); + } catch (FirebaseAuthException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Test error", e.getMessage()); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); + } + } + @Test public void testInvalidIAMCryptoSigner() { try { @@ -119,7 +147,7 @@ public void testInvalidIAMCryptoSigner() { } @Test - public void testMetadataService() throws IOException { + public void testMetadataService() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( ImmutableMap.of("signature", signature)); @@ -127,7 +155,7 @@ public void testMetadataService() throws IOException { ImmutableList.of( new MockLowLevelHttpResponse().setContent("metadata-server@iam.gserviceaccount.com"), new MockLowLevelHttpResponse().setContent(response))); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setHttpTransport(transport) .build(); @@ -142,11 +170,13 @@ public void testMetadataService() throws IOException { 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()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(url, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); } @Test - public void testExplicitServiceAccountEmail() throws IOException { + public void testExplicitServiceAccountEmail() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( ImmutableMap.of("signature", signature)); @@ -155,7 +185,7 @@ public void testExplicitServiceAccountEmail() throws IOException { MockHttpTransport transport = new MultiRequestMockHttpTransport( ImmutableList.of( new MockLowLevelHttpResponse().setContent(response))); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setServiceAccountId("explicit-service-account@iam.gserviceaccount.com") .setCredentials(new MockGoogleCredentialsWithSigner("test-token")) .setHttpTransport(transport) @@ -170,13 +200,15 @@ public void testExplicitServiceAccountEmail() throws IOException { 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()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(url, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); } @Test - public void testCredentialsWithSigner() throws IOException { + public void testCredentialsWithSigner() throws Exception { // Should fall back to signing-enabled credential - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentialsWithSigner("test-token")) .build(); FirebaseApp app = FirebaseApp.initializeApp(options, "customApp"); 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 1c85ceeee..875c781e9 100644 --- a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java +++ b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java @@ -29,8 +29,9 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.testing.TestUtils; -import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -158,12 +159,12 @@ private static class TestCryptoSigner implements CryptoSigner { } @Override - public byte[] sign(byte[] payload) throws IOException { + public byte[] sign(byte[] payload) throws FirebaseAuthException { try { return SecurityUtils.sign(SecurityUtils.getSha256WithRsaSignatureAlgorithm(), privateKey, payload); } catch (GeneralSecurityException e) { - throw new IOException(e); + throw new FirebaseAuthException(ErrorCode.UNKNOWN, "Failed to sign token", e, null, null); } } diff --git a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java index 36660dc28..4bc84cb4d 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java @@ -18,6 +18,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -26,6 +28,7 @@ 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; import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; import com.google.api.client.testing.http.MockHttpTransport; @@ -33,13 +36,14 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.AuthErrorCode; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.MockGoogleCredentials; -import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.internal.SdkUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; import com.google.firebase.testing.TestResponseInterceptor; @@ -90,7 +94,12 @@ public void testGetTenantWithNotFoundError() { FirebaseAuth.getInstance().getTenantManager().getTenant("UNKNOWN"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals( + "No tenant found for the given identifier (TENANT_NOT_FOUND).", e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.TENANT_NOT_FOUND, e.getAuthErrorCode()); } checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/UNKNOWN"); } @@ -183,16 +192,21 @@ public void testCreateTenantMinimal() throws Exception { @Test public void testCreateTenantError() { - TestResponseInterceptor interceptor = - initializeAppForTenantManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + TenantManager tenantManager = createRetryDisabledTenantManager(new MockLowLevelHttpResponse() + .setStatusCode(500) + .setContent(message)); + try { - FirebaseAuth.getInstance().getTenantManager().createTenant(new Tenant.CreateRequest()); + tenantManager.createTenant(new Tenant.CreateRequest()); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n" + message, e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "POST", TENANTS_BASE_URL); } @Test @@ -252,18 +266,23 @@ public void testUpdateTenantNoValues() throws Exception { @Test public void testUpdateTenantError() { - TestResponseInterceptor interceptor = - initializeAppForTenantManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + TenantManager tenantManager = createRetryDisabledTenantManager(new MockLowLevelHttpResponse() + .setStatusCode(500) + .setContent(message)); Tenant.UpdateRequest request = new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME"); + try { - FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + tenantManager.updateTenant(request); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n" + message, e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); } @Test @@ -285,7 +304,10 @@ public void testDeleteTenantWithNotFoundError() { FirebaseAuth.getInstance().getTenantManager().deleteTenant("UNKNOWN"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals("No tenant found for the given identifier (TENANT_NOT_FOUND).", e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); } checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/UNKNOWN"); } @@ -308,18 +330,22 @@ private static void checkRequestHeaders(TestResponseInterceptor interceptor) { private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) { HttpRequest request = interceptor.getResponse().getRequest(); - if (method.equals("PATCH")) { - assertEquals("PATCH", - request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override")); - assertEquals("POST", request.getRequestMethod()); - } else { - assertEquals(method, request.getRequestMethod()); - } + assertEquals(method, request.getRequestMethod()); assertEquals(url, request.getUrl().toString().split("\\?")[0]); } private static TestResponseInterceptor initializeAppForTenantManagement(String... responses) { - initializeAppWithResponses(responses); + List mocks = new ArrayList<>(); + for (String response : responses) { + mocks.add(new MockLowLevelHttpResponse().setContent(response)); + } + MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks); + FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(credentials) + .setHttpTransport(transport) + .setProjectId("test-project-id") + .build()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); FirebaseAuth.getInstance().getTenantManager().setInterceptor(interceptor); return interceptor; @@ -327,7 +353,7 @@ private static TestResponseInterceptor initializeAppForTenantManagement(String.. private static TestResponseInterceptor initializeAppForTenantManagementWithStatusCode( int statusCode, String response) { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .setHttpTransport( new MockHttpTransport.Builder() @@ -341,17 +367,16 @@ private static TestResponseInterceptor initializeAppForTenantManagementWithStatu return interceptor; } - private static void initializeAppWithResponses(String... responses) { - List mocks = new ArrayList<>(); - for (String response : responses) { - mocks.add(new MockLowLevelHttpResponse().setContent(response)); - } - MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks); - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + private static TenantManager createRetryDisabledTenantManager(MockLowLevelHttpResponse response) { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) - .setHttpTransport(transport) - .setProjectId("test-project-id") .build()); + FirebaseTenantClient tenantClient = new FirebaseTenantClient( + "test-project-id", Utils.getDefaultJsonFactory(), transport.createRequestFactory()); + return new TenantManager(app, tenantClient); } private static GenericJson parseRequestContent(TestResponseInterceptor interceptor) diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java index 61a841902..fd8e4af83 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java @@ -39,6 +39,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.AuthErrorCode; import com.google.firebase.auth.ExportedUserRecord; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuthException; @@ -257,8 +258,8 @@ public void testVerifyTokenWithWrongTenantAwareClient() throws Exception { fail("No error thrown for verifying a token with the wrong tenant-aware client"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals("tenant-id-mismatch", - ((FirebaseAuthException) e.getCause()).getErrorCode()); + assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, + ((FirebaseAuthException) e.getCause()).getAuthErrorCode()); } } diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java index df7bcf00d..db0099791 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java @@ -28,6 +28,7 @@ import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.base.Suppliers; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; @@ -58,7 +59,7 @@ public class TenantAwareFirebaseAuthTest { .build(); private static final FirebaseAuthException AUTH_EXCEPTION = new FirebaseAuthException( - "code", "reason"); + ErrorCode.INVALID_ARGUMENT, "Test error message", null, null, null); private static final String CREATE_COOKIE_RESPONSE = TestUtils.loadResource( "createSessionCookie.json"); @@ -99,7 +100,7 @@ public void testCreateSessionCookieAsyncError() throws Exception { } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); FirebaseAuthException cause = (FirebaseAuthException) e.getCause(); - assertEquals("code", cause.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, cause.getErrorCode()); } assertNull(interceptor.getResponse()); @@ -133,7 +134,7 @@ public void testCreateSessionCookieError() { auth.createSessionCookie("testToken", COOKIE_OPTIONS); fail("No error thrown for invalid ID token"); } catch (FirebaseAuthException e) { - assertEquals("code", e.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); } assertNull(interceptor.getResponse()); @@ -163,7 +164,7 @@ public void testVerifySessionCookieFailure() { auth.verifySessionCookie("cookie"); fail("No error thrown for invalid token"); } catch (FirebaseAuthException authException) { - assertEquals("code", authException.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, authException.getErrorCode()); assertEquals("cookie", tokenVerifier.getLastTokenString()); } } @@ -193,7 +194,7 @@ public void testVerifySessionCookieAsyncFailure() throws InterruptedException { fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("code", authException.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, authException.getErrorCode()); assertEquals("cookie", tokenVerifier.getLastTokenString()); } } @@ -224,7 +225,7 @@ public void testVerifySessionCookieWithCheckRevokedFailure() { auth.verifySessionCookie("cookie", true); fail("No error thrown for invalid token"); } catch (FirebaseAuthException e) { - assertEquals("code", e.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); assertEquals("cookie", tokenVerifier.getLastTokenString()); } } @@ -241,7 +242,7 @@ public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws Interru fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("code", authException.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, authException.getErrorCode()); assertEquals("cookie", tokenVerifier.getLastTokenString()); } } diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java index 086e25aa2..44d75e830 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java @@ -27,9 +27,10 @@ import com.google.api.core.ApiFutureCallback; import com.google.api.core.ApiFutures; import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.ErrorCode; +import com.google.firebase.auth.AuthErrorCode; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuthException; -import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.testing.IntegrationTestUtils; import java.util.ArrayList; import java.util.List; @@ -81,8 +82,9 @@ public void testTenantLifecycle() throws Exception { fail("No error thrown for getting a deleted tenant"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals(AuthErrorCode.TENANT_NOT_FOUND, authException.getAuthErrorCode()); } } diff --git a/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java b/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java index 03627f202..08ee49ff0 100644 --- a/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java +++ b/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java @@ -12,7 +12,6 @@ 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.auth.MockGoogleCredentials; @@ -23,11 +22,10 @@ public class FirestoreClientTest { - public static final FirestoreOptions FIRESTORE_OPTIONS = FirestoreOptions.newBuilder() + private static final FirestoreOptions FIRESTORE_OPTIONS = FirestoreOptions.newBuilder() // Setting credentials is not required (they get overridden by Admin SDK), but without // this Firestore logs an ugly warning during tests. .setCredentials(new MockGoogleCredentials("test-token")) - .setTimestampsInSnapshotsEnabled(true) .build(); @After @@ -37,7 +35,7 @@ public void tearDown() { @Test public void testExplicitProjectId() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setProjectId("explicit-project-id") .setFirestoreOptions(FIRESTORE_OPTIONS) @@ -51,7 +49,7 @@ public void testExplicitProjectId() throws IOException { @Test public void testServiceAccountProjectId() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setFirestoreOptions(FIRESTORE_OPTIONS) .build()); @@ -64,7 +62,7 @@ public void testServiceAccountProjectId() throws IOException { @Test public void testFirestoreOptions() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setProjectId("explicit-project-id") .setFirestoreOptions(FIRESTORE_OPTIONS) @@ -80,11 +78,10 @@ public void testFirestoreOptions() throws IOException { @Test public void testFirestoreOptionsOverride() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.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()) @@ -104,7 +101,7 @@ public void testFirestoreOptionsOverride() throws IOException { @Test public void testAppDelete() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setProjectId("mock-project-id") .setFirestoreOptions(FIRESTORE_OPTIONS) diff --git a/src/test/java/com/google/firebase/cloud/StorageClientTest.java b/src/test/java/com/google/firebase/cloud/StorageClientTest.java index 41535f3d9..190fad6cc 100644 --- a/src/test/java/com/google/firebase/cloud/StorageClientTest.java +++ b/src/test/java/com/google/firebase/cloud/StorageClientTest.java @@ -41,7 +41,7 @@ public void tearDown() { @Test public void testInvalidConfiguration() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .build()); try { @@ -54,7 +54,7 @@ public void testInvalidConfiguration() throws IOException { @Test public void testInvalidBucketName() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setStorageBucket("mock-bucket-name") .build()); @@ -75,7 +75,7 @@ public void testInvalidBucketName() throws IOException { @Test public void testAppDelete() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setStorageBucket("mock-bucket-name") .build()); @@ -93,7 +93,7 @@ public void testAppDelete() throws IOException { @Test public void testNonExistingBucket() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setStorageBucket("mock-bucket-name") .build()); @@ -115,7 +115,7 @@ public void testNonExistingBucket() throws IOException { @Test public void testBucket() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setStorageBucket("mock-bucket-name") .build()); diff --git a/src/test/java/com/google/firebase/database/DataSnapshotTest.java b/src/test/java/com/google/firebase/database/DataSnapshotTest.java index 0764bdbf4..083d8ce09 100644 --- a/src/test/java/com/google/firebase/database/DataSnapshotTest.java +++ b/src/test/java/com/google/firebase/database/DataSnapshotTest.java @@ -51,7 +51,7 @@ public class DataSnapshotTest { @BeforeClass public static void setUpClass() throws IOException { testApp = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); diff --git a/src/test/java/com/google/firebase/database/DatabaseReferenceTest.java b/src/test/java/com/google/firebase/database/DatabaseReferenceTest.java index 5e7fc5374..207173f74 100644 --- a/src/test/java/com/google/firebase/database/DatabaseReferenceTest.java +++ b/src/test/java/com/google/firebase/database/DatabaseReferenceTest.java @@ -64,7 +64,7 @@ public class DatabaseReferenceTest { @BeforeClass public static void setUpClass() throws IOException { testApp = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl(DB_URL) .build()); diff --git a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java index ba7816173..2bdc6185e 100644 --- a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java +++ b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java @@ -41,12 +41,12 @@ public class FirebaseDatabaseTest { private static final FirebaseOptions firebaseOptions = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://firebase-db-test.firebaseio.com") .build(); private static final FirebaseOptions firebaseOptionsWithoutDatabaseUrl = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .build(); diff --git a/src/test/java/com/google/firebase/database/TestHelpers.java b/src/test/java/com/google/firebase/database/TestHelpers.java index cfd15034d..00d9a3797 100644 --- a/src/test/java/com/google/firebase/database/TestHelpers.java +++ b/src/test/java/com/google/firebase/database/TestHelpers.java @@ -16,7 +16,6 @@ package com.google.firebase.database; -import static com.cedarsoftware.util.DeepEquals.deepEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -43,6 +42,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.Executors; @@ -241,7 +241,7 @@ public static void setHijackHash(DatabaseReference ref, boolean hijackHash) { * a true and more effective super set. */ public static void assertDeepEquals(Object a, Object b) { - if (!deepEquals(a, b)) { + if (!Objects.deepEquals(a, b)) { fail("Values different.\nExpected: " + a + "\nActual: " + b); } } diff --git a/src/test/java/com/google/firebase/database/ValueExpectationHelper.java b/src/test/java/com/google/firebase/database/ValueExpectationHelper.java index f7f15a03d..9732bf73b 100644 --- a/src/test/java/com/google/firebase/database/ValueExpectationHelper.java +++ b/src/test/java/com/google/firebase/database/ValueExpectationHelper.java @@ -18,10 +18,10 @@ import static org.junit.Assert.fail; -import com.cedarsoftware.util.DeepEquals; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.concurrent.Semaphore; public class ValueExpectationHelper { @@ -39,7 +39,7 @@ public void add(final Query query, final Object expected) { public void onDataChange(DataSnapshot snapshot) { Object result = snapshot.getValue(); // Hack to handle race condition in initial data - if (DeepEquals.deepEquals(expected, result)) { + if (Objects.deepEquals(expected, result)) { // We may pass through intermediate states, but we should end up with // the correct // state diff --git a/src/test/java/com/google/firebase/database/collection/RBTreeSortedMapTest.java b/src/test/java/com/google/firebase/database/collection/RBTreeSortedMapTest.java index 2fbc8ba90..f29ef208a 100644 --- a/src/test/java/com/google/firebase/database/collection/RBTreeSortedMapTest.java +++ b/src/test/java/com/google/firebase/database/collection/RBTreeSortedMapTest.java @@ -262,7 +262,7 @@ public void successorKeyIsCorrect() { lastKey = entry.getKey(); } if (lastKey != null) { - assertEquals(null, map.getSuccessorKey(lastKey)); + assertNull(map.getSuccessorKey(lastKey)); } } } @@ -295,13 +295,13 @@ public int compare(Integer o1, Integer o2) { arraycopy = arraycopy.insert(key, value); copyWithDifferentComparator = copyWithDifferentComparator.insert(key, value); } - Assert.assertTrue(map.equals(copy)); - Assert.assertTrue(map.equals(arraycopy)); - Assert.assertTrue(arraycopy.equals(map)); - - Assert.assertFalse(map.equals(copyWithDifferentComparator)); - Assert.assertFalse(map.equals(copy.remove(copy.getMaxKey()))); - Assert.assertFalse(map.equals(copy.insert(copy.getMaxKey() + 1, 1))); - Assert.assertFalse(map.equals(arraycopy.remove(arraycopy.getMaxKey()))); + Assert.assertEquals(map, copy); + Assert.assertEquals(map, arraycopy); + Assert.assertEquals(arraycopy, map); + + Assert.assertNotEquals(map, copyWithDifferentComparator); + Assert.assertNotEquals(map, copy.remove(copy.getMaxKey())); + Assert.assertNotEquals(map, copy.insert(copy.getMaxKey() + 1, 1)); + Assert.assertNotEquals(map, arraycopy.remove(arraycopy.getMaxKey())); } } diff --git a/src/test/java/com/google/firebase/database/core/JvmAuthTokenProviderTest.java b/src/test/java/com/google/firebase/database/core/JvmAuthTokenProviderTest.java index 7b8fe5229..9bc55950b 100644 --- a/src/test/java/com/google/firebase/database/core/JvmAuthTokenProviderTest.java +++ b/src/test/java/com/google/firebase/database/core/JvmAuthTokenProviderTest.java @@ -20,7 +20,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.cedarsoftware.util.DeepEquals; import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.OAuth2Credentials; import com.google.common.collect.ImmutableMap; @@ -38,6 +37,7 @@ import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -67,7 +67,7 @@ public void testGetToken() throws IOException, InterruptedException { credentials.refresh(); assertEquals(1, refreshDetector.count); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -87,7 +87,7 @@ public void testGetTokenNoRefresh() throws IOException, InterruptedException { credentials.refresh(); assertEquals(1, refreshDetector.count); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -103,7 +103,7 @@ public void testGetTokenNoRefresh() throws IOException, InterruptedException { public void testGetTokenWithAuthOverrides() throws InterruptedException, IOException { MockGoogleCredentials credentials = new MockGoogleCredentials("mock-token"); Map auth = ImmutableMap.of("uid", "test"); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .setDatabaseAuthVariableOverride(auth) .build(); @@ -119,11 +119,11 @@ public void testGetTokenWithAuthOverrides() throws InterruptedException, IOExcep public void testGetTokenError() throws InterruptedException { MockGoogleCredentials credentials = new MockGoogleCredentials("mock-token") { @Override - public AccessToken refreshAccessToken() throws IOException { + public AccessToken refreshAccessToken() { throw new RuntimeException("Test error"); } }; - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -139,13 +139,13 @@ public void testAddTokenChangeListener() throws IOException { final AtomicInteger counter = new AtomicInteger(0); MockGoogleCredentials credentials = new MockGoogleCredentials() { @Override - public AccessToken refreshAccessToken() throws IOException { + public AccessToken refreshAccessToken() { Date expiry = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); return new AccessToken("token-" + counter.getAndIncrement(), expiry); } }; - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -172,7 +172,7 @@ public void onTokenChange(String token) { @Test public void testTokenChangeListenerThread() throws InterruptedException, IOException { MockGoogleCredentials credentials = new MockGoogleCredentials(); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -210,12 +210,12 @@ public void testTokenAutoRefresh() throws InterruptedException { final Semaphore semaphore = new Semaphore(0); credentials.addChangeListener(new OAuth2Credentials.CredentialsChangedListener() { @Override - public void onChanged(OAuth2Credentials credentials) throws IOException { + public void onChanged(OAuth2Credentials credentials) { semaphore.release(); } }); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -234,8 +234,8 @@ private void assertToken(String token, String expectedToken, Map assertEquals(expectedToken, map.get("token")); - Map auth = (Map)map.get("auth"); - DeepEquals.deepEquals(expectedAuth, auth); + Map auth = (Map) map.get("auth"); + assertTrue(Objects.deepEquals(expectedAuth, auth)); } private static class TestGetTokenListener @@ -271,7 +271,7 @@ private static class TokenRefreshDetector private int count = 0; @Override - public void onChanged(OAuth2Credentials credentials) throws IOException { + public void onChanged(OAuth2Credentials credentials) { count++; } } diff --git a/src/test/java/com/google/firebase/database/core/JvmPlatformTest.java b/src/test/java/com/google/firebase/database/core/JvmPlatformTest.java index c6912916f..044b583b3 100644 --- a/src/test/java/com/google/firebase/database/core/JvmPlatformTest.java +++ b/src/test/java/com/google/firebase/database/core/JvmPlatformTest.java @@ -56,7 +56,7 @@ protected ThreadFactory getThreadFactory() { return Executors.defaultThreadFactory(); } }; - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .setThreadManager(threadManager) .build(); @@ -80,7 +80,7 @@ protected ThreadFactory getThreadFactory() { @Test public void userAgentHasCorrectParts() { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .build(); FirebaseApp app = FirebaseApp.initializeApp(options, "userAgentApp"); diff --git a/src/test/java/com/google/firebase/database/core/RepoTest.java b/src/test/java/com/google/firebase/database/core/RepoTest.java index ec9696e55..9c2281d4c 100644 --- a/src/test/java/com/google/firebase/database/core/RepoTest.java +++ b/src/test/java/com/google/firebase/database/core/RepoTest.java @@ -61,7 +61,7 @@ public class RepoTest { @BeforeClass public static void setUpClass() throws IOException { FirebaseApp testApp = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); diff --git a/src/test/java/com/google/firebase/database/core/SyncPointTest.java b/src/test/java/com/google/firebase/database/core/SyncPointTest.java index 91be0cce7..158a803e4 100644 --- a/src/test/java/com/google/firebase/database/core/SyncPointTest.java +++ b/src/test/java/com/google/firebase/database/core/SyncPointTest.java @@ -68,7 +68,7 @@ public class SyncPointTest { @BeforeClass public static void setUpClass() throws IOException { testApp = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); diff --git a/src/test/java/com/google/firebase/database/core/persistence/DefaultPersistenceManagerTest.java b/src/test/java/com/google/firebase/database/core/persistence/DefaultPersistenceManagerTest.java index 99450944c..342700a3d 100644 --- a/src/test/java/com/google/firebase/database/core/persistence/DefaultPersistenceManagerTest.java +++ b/src/test/java/com/google/firebase/database/core/persistence/DefaultPersistenceManagerTest.java @@ -56,7 +56,7 @@ public class DefaultPersistenceManagerTest { @BeforeClass public static void setUpClass() throws IOException { testApp = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); diff --git a/src/test/java/com/google/firebase/database/core/persistence/RandomPersistenceTest.java b/src/test/java/com/google/firebase/database/core/persistence/RandomPersistenceTest.java index 58358a8ff..d55db51c3 100644 --- a/src/test/java/com/google/firebase/database/core/persistence/RandomPersistenceTest.java +++ b/src/test/java/com/google/firebase/database/core/persistence/RandomPersistenceTest.java @@ -73,7 +73,7 @@ public class RandomPersistenceTest { @BeforeClass public static void setUpClass() throws IOException { testApp = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); diff --git a/src/test/java/com/google/firebase/database/integration/DataTestIT.java b/src/test/java/com/google/firebase/database/integration/DataTestIT.java index 2b2124a14..d832f995d 100644 --- a/src/test/java/com/google/firebase/database/integration/DataTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/DataTestIT.java @@ -24,7 +24,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.cedarsoftware.util.DeepEquals; import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.database.ChildEventListener; @@ -56,6 +55,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Random; import java.util.concurrent.ExecutionException; import java.util.concurrent.Semaphore; @@ -1645,7 +1645,7 @@ public void testUpdateAfterSetLeafNodeWorks() throws InterruptedException { ref.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { - if (DeepEquals.deepEquals(snapshot.getValue(), expected)) { + if (Objects.deepEquals(snapshot.getValue(), expected)) { semaphore.release(); } } diff --git a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java index d210039cc..58e313450 100644 --- a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java @@ -78,7 +78,7 @@ public void testAuthWithValidCertificateCredential() throws InterruptedException @Test public void testAuthWithInvalidCertificateCredential() throws InterruptedException, IOException { FirebaseOptions options = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setDatabaseUrl(IntegrationTestUtils.getDatabaseUrl()) .setCredentials(GoogleCredentials.fromStream(ServiceAccount.NONE.asStream())) .build(); @@ -94,10 +94,9 @@ public void testDatabaseAuthVariablesAuthorization() throws InterruptedException "uid", "test", "custom", "secret" ); - FirebaseOptions options = - new FirebaseOptions.Builder(masterApp.getOptions()) - .setDatabaseAuthVariableOverride(authVariableOverrides) - .build(); + FirebaseOptions options = masterApp.getOptions().toBuilder() + .setDatabaseAuthVariableOverride(authVariableOverrides) + .build(); FirebaseApp testUidApp = FirebaseApp.initializeApp(options, "testGetAppWithUid"); FirebaseDatabase masterDb = FirebaseDatabase.getInstance(masterApp); FirebaseDatabase testAuthOverridesDb = FirebaseDatabase.getInstance(testUidApp); @@ -114,10 +113,9 @@ public void testDatabaseAuthVariablesAuthorization() throws InterruptedException @Test public void testDatabaseAuthVariablesNoAuthorization() throws InterruptedException { - FirebaseOptions options = - new FirebaseOptions.Builder(masterApp.getOptions()) - .setDatabaseAuthVariableOverride(null) - .build(); + FirebaseOptions options = masterApp.getOptions().toBuilder() + .setDatabaseAuthVariableOverride(null) + .build(); FirebaseApp testUidApp = FirebaseApp.initializeApp(options, "testServiceAccountDatabaseWithNoAuth"); diff --git a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java index f6451d9c5..84ec321a5 100644 --- a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java @@ -255,7 +255,7 @@ public void onCancelled(DatabaseError error) { private static FirebaseApp appWithDbUrl(String dbUrl, String name) { try { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setDatabaseUrl(dbUrl) .setCredentials(GoogleCredentials.fromStream( IntegrationTestUtils.getServiceAccountCertificate())) @@ -268,7 +268,7 @@ private static FirebaseApp appWithDbUrl(String dbUrl, String name) { private static FirebaseApp appWithoutDbUrl(String name) { try { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream( IntegrationTestUtils.getServiceAccountCertificate())) .build(); diff --git a/src/test/java/com/google/firebase/database/integration/RulesTestIT.java b/src/test/java/com/google/firebase/database/integration/RulesTestIT.java index 1745ce220..b46c1287f 100644 --- a/src/test/java/com/google/firebase/database/integration/RulesTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/RulesTestIT.java @@ -103,7 +103,7 @@ public class RulesTestIT { public static void setUpClass() throws IOException { // Init app with non-admin privileges Map auth = MapBuilder.of("uid", "my-service-worker"); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream( IntegrationTestUtils.getServiceAccountCertificate())) .setDatabaseUrl(IntegrationTestUtils.getDatabaseUrl()) diff --git a/src/test/java/com/google/firebase/database/integration/ShutdownExample.java b/src/test/java/com/google/firebase/database/integration/ShutdownExample.java index 020e0416e..ded28fe0f 100644 --- a/src/test/java/com/google/firebase/database/integration/ShutdownExample.java +++ b/src/test/java/com/google/firebase/database/integration/ShutdownExample.java @@ -32,7 +32,7 @@ public static void main(String[] args) { FirebaseApp app = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); diff --git a/src/test/java/com/google/firebase/database/utilities/ValidationTest.java b/src/test/java/com/google/firebase/database/utilities/ValidationTest.java index b9e32a851..3293f5d6d 100644 --- a/src/test/java/com/google/firebase/database/utilities/ValidationTest.java +++ b/src/test/java/com/google/firebase/database/utilities/ValidationTest.java @@ -18,9 +18,11 @@ import static org.junit.Assert.fail; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.database.DatabaseException; import com.google.firebase.database.core.Path; +import java.util.List; import java.util.Map; import org.junit.Test; @@ -144,32 +146,32 @@ public void testNonWritablePath() { @Test public void testUpdate() { - Map[] updates = new Map[]{ - ImmutableMap.of("foo", "value"), - ImmutableMap.of("foo", ""), - ImmutableMap.of("foo", 10D), - ImmutableMap.of(".foo", "foo"), - ImmutableMap.of("foo", "value", "bar", "value"), - }; + List> updates = ImmutableList.>of( + ImmutableMap.of("foo", "value"), + ImmutableMap.of("foo", ""), + ImmutableMap.of("foo", 10D), + ImmutableMap.of(".foo", "foo"), + ImmutableMap.of("foo", "value", "bar", "value") + ); Path path = new Path("path"); - for (Map map : updates) { + for (Map map : updates) { Validation.parseAndValidateUpdate(path, map); } } @Test public void testInvalidUpdate() { - Map[] invalidUpdates = new Map[]{ - ImmutableMap.of(".sv", "foo"), - ImmutableMap.of(".value", "foo"), - ImmutableMap.of(".priority", ImmutableMap.of("a", "b")), - ImmutableMap.of("foo", "value", "foo/bar", "value"), - ImmutableMap.of("foo", Double.POSITIVE_INFINITY), - ImmutableMap.of("foo", Double.NEGATIVE_INFINITY), - ImmutableMap.of("foo", Double.NaN), - }; + List> invalidUpdates = ImmutableList.>of( + ImmutableMap.of(".sv", "foo"), + ImmutableMap.of(".value", "foo"), + ImmutableMap.of(".priority", ImmutableMap.of("a", "b")), + ImmutableMap.of("foo", "value", "foo/bar", "value"), + ImmutableMap.of("foo", Double.POSITIVE_INFINITY), + ImmutableMap.of("foo", Double.NEGATIVE_INFINITY), + ImmutableMap.of("foo", Double.NaN) + ); Path path = new Path("path"); - for (Map map : invalidUpdates) { + for (Map map : invalidUpdates) { try { Validation.parseAndValidateUpdate(path, map); fail("No error thrown for invalid update: " + map); diff --git a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java index 366bb7e29..f15861a62 100644 --- a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java +++ b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java @@ -23,18 +23,25 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpTransport; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.OutgoingHttpRequest; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.testing.GenericFunction; import com.google.firebase.testing.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; +import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -43,6 +50,25 @@ public class FirebaseInstanceIdTest { + private static final Map ERROR_MESSAGES = ImmutableMap.of( + 404, "Instance ID \"test-iid\": Failed to find the instance ID.", + 409, "Instance ID \"test-iid\": Already deleted.", + 429, "Instance ID \"test-iid\": Request throttled out by the backend server.", + 500, "Instance ID \"test-iid\": Internal server error.", + 501, "Unexpected HTTP response with status: 501\ntest error" + ); + + private static final Map ERROR_CODES = ImmutableMap.of( + 404, ErrorCode.NOT_FOUND, + 409, ErrorCode.CONFLICT, + 429, ErrorCode.RESOURCE_EXHAUSTED, + 500, ErrorCode.INTERNAL, + 501, ErrorCode.UNKNOWN + ); + + private static final String TEST_URL = + "https://console.firebase.google.com/v1/project/test-project/instanceId/test-iid"; + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -50,7 +76,7 @@ public void tearDown() { @Test public void testNoProjectId() { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .build(); FirebaseApp.initializeApp(options); @@ -64,7 +90,7 @@ public void testNoProjectId() { @Test public void testInvalidInstanceId() { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); @@ -96,7 +122,7 @@ public void testDeleteInstanceId() throws Exception { MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(response) .build(); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .setHttpTransport(transport) @@ -123,7 +149,6 @@ public Void call(Object... args) throws Exception { } ); - String url = "https://console.firebase.google.com/v1/project/test-project/instanceId/test-iid"; for (GenericFunction fn : functions) { TestResponseInterceptor interceptor = new TestResponseInterceptor(); instanceId.setInterceptor(interceptor); @@ -131,54 +156,113 @@ public Void call(Object... args) throws Exception { assertNotNull(interceptor.getResponse()); HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("DELETE", request.getRequestMethod()); - assertEquals(url, request.getUrl().toString()); + assertEquals(HttpMethods.DELETE, request.getRequestMethod()); + assertEquals(TEST_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); } } @Test public void testDeleteInstanceIdError() throws Exception { - Map errors = ImmutableMap.of( - 404, "Instance ID \"test-iid\": Failed to find the instance ID.", - 429, "Instance ID \"test-iid\": Request throttled out by the backend server.", - 500, "Instance ID \"test-iid\": Internal server error.", - 501, "Error while invoking instance ID service." - ); + final MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); - String url = "https://console.firebase.google.com/v1/project/test-project/instanceId/test-iid"; - for (Map.Entry entry : errors.entrySet()) { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setStatusCode(entry.getKey()) - .setContent("test error"); - MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(response) - .build(); - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .setHttpTransport(transport) - .build(); - final FirebaseApp app = FirebaseApp.initializeApp(options); - - FirebaseInstanceId instanceId = FirebaseInstanceId.getInstance(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - instanceId.setInterceptor(interceptor); - try { - instanceId.deleteInstanceIdAsync("test-iid").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseInstanceIdException); - assertEquals(entry.getValue(), e.getCause().getMessage()); - assertTrue(e.getCause().getCause() instanceof HttpResponseException); - } + // Disable retries by passing a regular HttpRequestFactory. + FirebaseInstanceId instanceId = new FirebaseInstanceId(app, transport.createRequestFactory()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + instanceId.setInterceptor(interceptor); - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("DELETE", request.getRequestMethod()); - assertEquals(url, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + try { + for (int statusCode : ERROR_CODES.keySet()) { + response.setStatusCode(statusCode).setContent("test error"); + + try { + instanceId.deleteInstanceIdAsync("test-iid").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseInstanceIdException); + checkFirebaseInstanceIdException((FirebaseInstanceIdException) e.getCause(), statusCode); + } + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(HttpMethods.DELETE, request.getRequestMethod()); + assertEquals(TEST_URL, request.getUrl().toString()); + } + } finally { app.delete(); } } + + @Test + public void testDeleteInstanceIdTransportError() throws Exception { + HttpTransport transport = TestUtils.createFaultyHttpTransport(); + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + // Disable retries by passing a regular HttpRequestFactory. + FirebaseInstanceId instanceId = new FirebaseInstanceId(app, transport.createRequestFactory()); + + try { + instanceId.deleteInstanceIdAsync("test-iid").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseInstanceIdException); + FirebaseInstanceIdException error = (FirebaseInstanceIdException) e.getCause(); + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertEquals( + "Unknown error while making a remote service call: transport error", + error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + assertNull(error.getHttpResponse()); + } + } + + @Test + public void testDeleteInstanceIdInvalidJsonIgnored() throws Exception { + final MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + + // Disable retries by passing a regular HttpRequestFactory. + FirebaseInstanceId instanceId = new FirebaseInstanceId(app, transport.createRequestFactory()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + instanceId.setInterceptor(interceptor); + response.setContent("not json"); + + instanceId.deleteInstanceIdAsync("test-iid").get(); + + assertNotNull(interceptor.getResponse()); + } + + private void checkFirebaseInstanceIdException(FirebaseInstanceIdException error, int statusCode) { + assertEquals(ERROR_CODES.get(statusCode), error.getErrorCode()); + assertEquals(ERROR_MESSAGES.get(statusCode), error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + + IncomingHttpResponse httpResponse = error.getHttpResponse(); + assertNotNull(httpResponse); + assertEquals(statusCode, httpResponse.getStatusCode()); + OutgoingHttpRequest request = httpResponse.getRequest(); + assertEquals(HttpMethods.DELETE, request.getMethod()); + assertEquals(TEST_URL, request.getUrl()); + } } diff --git a/src/test/java/com/google/firebase/internal/AbstractPlatformErrorHandlerTest.java b/src/test/java/com/google/firebase/internal/AbstractPlatformErrorHandlerTest.java new file mode 100644 index 000000000..7e927f51b --- /dev/null +++ b/src/test/java/com/google/firebase/internal/AbstractPlatformErrorHandlerTest.java @@ -0,0 +1,298 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +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.HttpMethods; +import com.google.api.client.http.HttpStatusCodes; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.api.client.util.GenericData; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import java.io.IOException; +import java.net.NoRouteToHostException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import org.junit.Test; + +public class AbstractPlatformErrorHandlerTest { + + private static final HttpRequestInfo TEST_REQUEST = HttpRequestInfo.buildGetRequest( + "https://firebase.google.com"); + + @Test + public void testPlatformError() { + String payload = "{\"error\": {\"status\": \"UNAVAILABLE\", \"message\": \"Test error\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.UNAVAILABLE, e.getErrorCode()); + assertEquals("Test error", e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testNonJsonError() { + String payload = "not json"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\nnot json", e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testPlatformErrorWithoutCode() { + String payload = "{\"error\": {\"message\": \"Test error\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Test error", e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testPlatformErrorWithoutMessage() { + String payload = "{\"error\": {\"status\": \"INVALID_ARGUMENT\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n" + payload, e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testPlatformErrorWithoutCodeOrMessage() { + String payload = "{}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n" + payload, e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testGenericIOException() { + IOException exception = new IOException("Test"); + ErrorHandlingHttpClient client = createHttpClient(exception); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals( + "Unknown error while making a remote service call: Test", e.getMessage()); + assertNull(e.getHttpResponse()); + assertSame(exception, e.getCause()); + } + } + + @Test + public void testTimeoutError() { + IOException exception = new IOException("Test", new SocketTimeoutException()); + ErrorHandlingHttpClient client = createHttpClient(exception); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, e.getErrorCode()); + assertEquals( + "Timed out while making an API call: Test", e.getMessage()); + assertNull(e.getHttpResponse()); + assertSame(exception, e.getCause()); + } + } + + @Test + public void testNoRouteToHostError() { + IOException exception = new IOException("Test", new NoRouteToHostException()); + ErrorHandlingHttpClient client = createHttpClient(exception); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.UNAVAILABLE, e.getErrorCode()); + assertEquals( + "Failed to establish a connection: Test", e.getMessage()); + assertNull(e.getHttpResponse()); + assertSame(exception, e.getCause()); + } + } + + @Test + public void testUnknownHostError() { + IOException exception = new IOException("Test", new UnknownHostException()); + ErrorHandlingHttpClient client = createHttpClient(exception); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.UNAVAILABLE, e.getErrorCode()); + assertEquals( + "Failed to establish a connection: Test", e.getMessage()); + assertNull(e.getHttpResponse()); + assertSame(exception, e.getCause()); + } + } + + @Test + public void testParseError() { + String payload = "not json"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertTrue(e.getMessage().startsWith("Error while parsing HTTP response: ")); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_OK, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testUnknownHttpError() { + String payload = "{\"error\": {\"message\": \"Test error\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(512) + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("Test error", e.getMessage()); + assertHttpResponse(e, 512, payload); + assertNotNull(e.getCause()); + } + } + + private ErrorHandlingHttpClient createHttpClient( + MockLowLevelHttpResponse response) { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + return new ErrorHandlingHttpClient<>( + transport.createRequestFactory(), + Utils.getDefaultJsonFactory(), + new TestPlatformErrorHandler()); + } + + private ErrorHandlingHttpClient createHttpClient( + final IOException exception) { + MockHttpTransport transport = new MockHttpTransport(){ + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + throw exception; + } + }; + return new ErrorHandlingHttpClient<>( + transport.createRequestFactory(), + Utils.getDefaultJsonFactory(), + new TestPlatformErrorHandler()); + } + + private void assertHttpResponse(FirebaseException e, int statusCode, String content) { + IncomingHttpResponse httpResponse = e.getHttpResponse(); + assertNotNull(httpResponse); + assertEquals(statusCode, httpResponse.getStatusCode()); + assertEquals(content, httpResponse.getContent()); + assertEquals(HttpMethods.GET, httpResponse.getRequest().getMethod()); + } + + private static class TestPlatformErrorHandler extends + AbstractPlatformErrorHandler { + + TestPlatformErrorHandler() { + super(Utils.getDefaultJsonFactory()); + } + + @Override + protected MockFirebaseException createException(FirebaseException base) { + return new MockFirebaseException(base); + } + } + + private static class MockFirebaseException extends FirebaseException { + MockFirebaseException(FirebaseException base) { + super(base.getErrorCode(), base.getMessage(), base.getCause(), base.getHttpResponse()); + } + } +} diff --git a/src/test/java/com/google/firebase/internal/CallableOperationTest.java b/src/test/java/com/google/firebase/internal/CallableOperationTest.java index 2ead9a9c7..d38c96776 100644 --- a/src/test/java/com/google/firebase/internal/CallableOperationTest.java +++ b/src/test/java/com/google/firebase/internal/CallableOperationTest.java @@ -24,7 +24,6 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; 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.auth.MockGoogleCredentials; import com.google.firebase.internal.FirebaseThreadManagers.GlobalThreadManager; @@ -38,7 +37,7 @@ public class CallableOperationTest { private static final String TEST_FIREBASE_THREAD = "test-firebase-thread"; - private static final FirebaseOptions OPTIONS = new Builder() + private static final FirebaseOptions OPTIONS = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .setThreadManager(new MockThreadManager()) .build(); diff --git a/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java b/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java new file mode 100644 index 000000000..591914002 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java @@ -0,0 +1,358 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.ByteArrayContent; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpStatusCodes; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.api.client.testing.util.MockSleeper; +import com.google.api.client.util.GenericData; +import com.google.auth.oauth2.AccessToken; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.testing.TestResponseInterceptor; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.junit.Test; + +public class ErrorHandlingHttpClientTest { + + private static final JsonFactory DEFAULT_JSON_FACTORY = Utils.getDefaultJsonFactory(); + + private static final HttpRequestInfo TEST_REQUEST = HttpRequestInfo.buildGetRequest( + "https://firebase.google.com"); + + @Test(expected = NullPointerException.class) + public void testNullRequestFactory() { + new ErrorHandlingHttpClient<>( + null, + DEFAULT_JSON_FACTORY, + new TestHttpErrorHandler()); + } + + @Test(expected = NullPointerException.class) + public void testNullJsonFactory() { + new ErrorHandlingHttpClient<>( + Utils.getDefaultTransport().createRequestFactory(), + null, + new TestHttpErrorHandler()); + } + + @Test(expected = NullPointerException.class) + public void testNullErrorHandler() { + new ErrorHandlingHttpClient<>( + Utils.getDefaultTransport().createRequestFactory(), + DEFAULT_JSON_FACTORY, + null); + } + + @Test + public void testSuccessfulRequest() throws FirebaseException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("{\"foo\": \"bar\"}"); + ErrorHandlingHttpClient client = createHttpClient(response); + + GenericData body = client.sendAndParse(TEST_REQUEST, GenericData.class); + + assertEquals(1, body.size()); + assertEquals("bar", body.get("foo")); + } + + @Test + public void testSuccessfulRequestWithoutContent() throws FirebaseException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setZeroContent(); + ErrorHandlingHttpClient client = createHttpClient(response); + + IncomingHttpResponse responseInfo = client.send(TEST_REQUEST); + + assertEquals(HttpStatusCodes.STATUS_CODE_OK, responseInfo.getStatusCode()); + assertNull(responseInfo.getContent()); + } + + @Test + public void testSuccessfulRequestWithHeadersAndBody() throws FirebaseException, IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("{\"foo\": \"bar\"}"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + ErrorHandlingHttpClient client = createHttpClient(response) + .setInterceptor(interceptor); + + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest( + "https://firebase.google.com", ImmutableMap.of("key", "value")); + + request.addHeader("h1", "v1") + .addAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")); + GenericData body = client.sendAndParse(request, GenericData.class); + + assertEquals(1, body.size()); + assertEquals("bar", body.get("foo")); + HttpRequest last = interceptor.getLastRequest(); + assertEquals(HttpMethods.POST, last.getRequestMethod()); + assertEquals("v1", last.getHeaders().get("h1")); + assertEquals("v2", last.getHeaders().get("h2")); + assertEquals("v3", last.getHeaders().get("h3")); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + last.getContent().writeTo(out); + assertEquals("{\"key\":\"value\"}", out.toString()); + } + + @Test + public void testSuccessfulRequestWithNonJsonBody() throws FirebaseException, IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("{\"foo\": \"bar\"}"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + ErrorHandlingHttpClient client = createHttpClient(response) + .setInterceptor(interceptor); + HttpContent content = new ByteArrayContent("text/plain", "Test".getBytes()); + + HttpRequestInfo request = HttpRequestInfo.buildRequest( + HttpMethods.POST, "https://firebase.google.com", content); + + GenericData body = client.sendAndParse(request, GenericData.class); + + assertEquals(1, body.size()); + assertEquals("bar", body.get("foo")); + HttpRequest last = interceptor.getLastRequest(); + assertEquals(HttpMethods.POST, last.getRequestMethod()); + assertEquals("text/plain", last.getContent().getType()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + last.getContent().writeTo(out); + assertEquals("Test", out.toString()); + } + + @Test + public void testUnsupportedMethod() throws FirebaseException, IOException { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent("{}")) + .setSupportedMethods(ImmutableSet.of(HttpMethods.GET, HttpMethods.POST)) + .build(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + ErrorHandlingHttpClient client = new ErrorHandlingHttpClient<>( + transport.createRequestFactory(), DEFAULT_JSON_FACTORY, new TestHttpErrorHandler()); + client.setInterceptor(interceptor); + HttpRequestInfo patchRequest = HttpRequestInfo.buildJsonRequest( + HttpMethods.PATCH, "https://firebase.google.com", ImmutableMap.of("key", "value")); + + client.sendAndParse(patchRequest, GenericData.class); + + HttpRequest last = interceptor.getLastRequest(); + assertEquals(HttpMethods.POST, last.getRequestMethod()); + assertEquals(HttpMethods.PATCH, last.getHeaders().get("X-HTTP-Method-Override")); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + last.getContent().writeTo(out); + assertEquals("{\"key\":\"value\"}", out.toString()); + } + + @Test + public void testNetworkError() { + final IOException exception = new IOException("Test"); + MockHttpTransport transport = new MockHttpTransport(){ + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + throw exception; + } + }; + ErrorHandlingHttpClient client = new ErrorHandlingHttpClient<>( + transport.createRequestFactory(), + DEFAULT_JSON_FACTORY, + new TestHttpErrorHandler()); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Test", e.getMessage()); + assertNull(e.getHttpResponse()); + assertSame(exception, e.getCause()); + } + } + + @Test + public void testErrorResponse() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .addHeader("Custom-Header", "value") + .setContent("{}"); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Example error message: {}", e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, "{}"); + IncomingHttpResponse httpResponse = e.getHttpResponse(); + assertEquals(1, httpResponse.getHeaders().size()); + assertEquals(ImmutableList.of("value"), httpResponse.getHeaders().get("custom-header")); + assertNotNull(e.getCause()); + } + } + + @Test + public void testParseError() { + String payload = "not json"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("Parse error", e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_OK, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testRetryOnError() { + CountingLowLevelHttpRequest request = CountingLowLevelHttpRequest.fromStatus(503); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpRequest(request) + .build(); + + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("token")) + .setHttpTransport(transport) + .build()); + RetryConfig retryConfig = RetryConfig.builder() + .setMaxRetries(4) + .setRetryStatusCodes(ImmutableList.of(503)) + .setSleeper(new MockSleeper()) + .build(); + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory( + app, retryConfig); + ErrorHandlingHttpClient client = new ErrorHandlingHttpClient<>( + requestFactory, Utils.getDefaultJsonFactory(), new TestHttpErrorHandler()); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Example error message: null", e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE, null); + assertNotNull(e.getCause()); + + assertEquals(5, request.getCount()); + } finally { + app.delete(); + } + } + + @Test + public void testRequestInitializationError() { + CountingLowLevelHttpRequest request = CountingLowLevelHttpRequest.fromStatus(503); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpRequest(request) + .build(); + + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials() { + @Override + public AccessToken refreshAccessToken() throws IOException { + throw new IOException("Failed to fetch credentials"); + } + }) + .setHttpTransport(transport) + .build()); + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + ErrorHandlingHttpClient client = new ErrorHandlingHttpClient<>( + requestFactory, Utils.getDefaultJsonFactory(), new TestHttpErrorHandler()); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Failed to fetch credentials", e.getMessage()); + assertNull(e.getHttpResponse()); + assertNotNull(e.getCause()); + } finally { + app.delete(); + } + } + + private ErrorHandlingHttpClient createHttpClient( + MockLowLevelHttpResponse response) { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + return new ErrorHandlingHttpClient<>( + transport.createRequestFactory(), + DEFAULT_JSON_FACTORY, + new TestHttpErrorHandler()); + } + + private void assertHttpResponse(FirebaseException e, int statusCode, String content) { + IncomingHttpResponse httpResponse = e.getHttpResponse(); + assertNotNull(httpResponse); + assertEquals(statusCode, httpResponse.getStatusCode()); + assertEquals(content, httpResponse.getContent()); + assertEquals("GET", httpResponse.getRequest().getMethod()); + } + + private static class TestHttpErrorHandler implements HttpErrorHandler { + @Override + public FirebaseException handleIOException(IOException e) { + return new FirebaseException( + ErrorCode.UNKNOWN, "IO error: " + e.getMessage(), e); + } + + @Override + public FirebaseException handleHttpResponseException( + HttpResponseException e, IncomingHttpResponse response) { + return new FirebaseException( + ErrorCode.INTERNAL, "Example error message: " + e.getContent(), e, response); + } + + @Override + public FirebaseException handleParseException(IOException e, IncomingHttpResponse response) { + return new FirebaseException(ErrorCode.UNKNOWN, "Parse error", e, response); + } + } +} diff --git a/src/test/java/com/google/firebase/internal/FirebaseAppStoreTest.java b/src/test/java/com/google/firebase/internal/FirebaseAppStoreTest.java index 4b3699d82..b6b502fdc 100644 --- a/src/test/java/com/google/firebase/internal/FirebaseAppStoreTest.java +++ b/src/test/java/com/google/firebase/internal/FirebaseAppStoreTest.java @@ -16,8 +16,6 @@ package com.google.firebase.internal; -import static org.junit.Assert.assertTrue; - import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; @@ -34,7 +32,7 @@ public class FirebaseAppStoreTest { private static final String FIREBASE_DB_URL = "https://mock-project.firebaseio.com"; private static final FirebaseOptions ALL_VALUES_OPTIONS = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setDatabaseUrl(FIREBASE_DB_URL) .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .build(); @@ -55,7 +53,7 @@ public void incompatibleAppInitializedDoesntThrow() throws IOException { FirebaseApp.initializeApp(ALL_VALUES_OPTIONS, name); TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); FirebaseOptions options = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .build(); FirebaseApp.initializeApp(options, name); @@ -66,18 +64,9 @@ public void incompatibleDefaultAppInitializedDoesntThrow() throws IOException { FirebaseApp.initializeApp(ALL_VALUES_OPTIONS); TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); FirebaseOptions options = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .build(); FirebaseApp.initializeApp(options); } - - @Test - public void persistenceDisabled() { - String name = "myApp"; - FirebaseApp.initializeApp(ALL_VALUES_OPTIONS, name); - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); - FirebaseAppStore appStore = FirebaseAppStore.getInstance(); - assertTrue(!appStore.getAllPersistedAppNames().contains(name)); - } } diff --git a/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java b/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java index 1f610f5e3..fde2f918b 100644 --- a/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java +++ b/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java @@ -46,7 +46,7 @@ public void tearDown() { @Test public void testDefaultSettings() throws Exception { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("token")) .build()); HttpRequest request = TestUtils.createRequest(); @@ -65,7 +65,7 @@ public void testDefaultSettings() throws Exception { @Test public void testExplicitTimeouts() throws Exception { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("token")) .setConnectTimeout(CONNECT_TIMEOUT_MILLIS) .setReadTimeout(READ_TIMEOUT_MILLIS) @@ -85,7 +85,7 @@ public void testExplicitTimeouts() throws Exception { @Test public void testRetryConfig() throws Exception { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("token")) .build()); RetryConfig retryConfig = RetryConfig.builder() @@ -106,7 +106,7 @@ public void testRetryConfig() throws Exception { @Test public void testRetryConfigWithIOExceptionHandling() throws Exception { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("token")) .build()); RetryConfig retryConfig = RetryConfig.builder() @@ -128,7 +128,7 @@ public void testRetryConfigWithIOExceptionHandling() throws Exception { @Test public void testCredentialsRetryHandler() throws Exception { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("token")) .build()); RetryConfig retryConfig = RetryConfig.builder() diff --git a/src/test/java/com/google/firebase/internal/FirebaseThreadManagersTest.java b/src/test/java/com/google/firebase/internal/FirebaseThreadManagersTest.java index 2b117f29f..286cdf09f 100644 --- a/src/test/java/com/google/firebase/internal/FirebaseThreadManagersTest.java +++ b/src/test/java/com/google/firebase/internal/FirebaseThreadManagersTest.java @@ -49,7 +49,7 @@ public void tearDown() { @Test public void testGlobalThreadManager() { MockThreadManager threadManager = new MockThreadManager(); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .setThreadManager(threadManager) .build(); @@ -74,7 +74,7 @@ public void testGlobalThreadManager() { @Test public void testGlobalThreadManagerWithMultipleApps() { MockThreadManager threadManager = new MockThreadManager(); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .build(); FirebaseApp defaultApp = FirebaseApp.initializeApp(options); @@ -99,7 +99,7 @@ public void testGlobalThreadManagerWithMultipleApps() { @Test public void testGlobalThreadManagerReInit() { MockThreadManager threadManager = new MockThreadManager(); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .setThreadManager(threadManager) .build(); @@ -125,7 +125,7 @@ public void testGlobalThreadManagerReInit() { @Test public void testDefaultThreadManager() throws Exception { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .build(); FirebaseApp defaultApp = FirebaseApp.initializeApp(options); diff --git a/src/test/java/com/google/firebase/internal/TestApiClientUtils.java b/src/test/java/com/google/firebase/internal/TestApiClientUtils.java index 9f4bc2d09..510e08cd5 100644 --- a/src/test/java/com/google/firebase/internal/TestApiClientUtils.java +++ b/src/test/java/com/google/firebase/internal/TestApiClientUtils.java @@ -18,17 +18,14 @@ import static com.google.firebase.internal.ApiClientUtils.DEFAULT_RETRY_CONFIG; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -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.HttpUnsuccessfulResponseHandler; import com.google.api.client.testing.util.MockSleeper; import com.google.firebase.FirebaseApp; import com.google.firebase.internal.RetryInitializer.RetryHandlerDecorator; -import java.io.IOException; public class TestApiClientUtils { @@ -39,8 +36,6 @@ public class TestApiClientUtils { .setSleeper(new MockSleeper()) .build(); - private static final GenericUrl TEST_URL = new GenericUrl("https://firebase.google.com"); - /** * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and * automatic retries. Bypasses exponential backoff between consecutive retries for faster @@ -65,20 +60,12 @@ public static HttpRequestFactory retryDisabledRequestFactory(FirebaseApp app) { } /** - * Checks whther the given HttpRequestFactory has been configured for authorization and + * Checks whether the given HttpRequest has been configured for authorization and * automatic retries. * - * @param requestFactory The HttpRequestFactory to check. + * @param request The HttpRequest to check. */ - public static void assertAuthAndRetrySupport(HttpRequestFactory requestFactory) { - assertTrue(requestFactory.getInitializer() instanceof FirebaseRequestInitializer); - HttpRequest request; - try { - request = requestFactory.buildGetRequest(TEST_URL); - } catch (IOException e) { - throw new RuntimeException("Failed to initialize request", e); - } - + public static void assertAuthAndRetrySupport(HttpRequest request) { // Verify authorization assertTrue(request.getHeaders().getAuthorization().startsWith("Bearer ")); @@ -89,7 +76,7 @@ public static void assertAuthAndRetrySupport(HttpRequestFactory requestFactory) .getRetryConfig(); assertEquals(DEFAULT_RETRY_CONFIG.getMaxRetries(), retryConfig.getMaxRetries()); assertEquals(DEFAULT_RETRY_CONFIG.getMaxIntervalMillis(), retryConfig.getMaxIntervalMillis()); - assertFalse(retryConfig.isRetryOnIOExceptions()); + assertEquals(DEFAULT_RETRY_CONFIG.isRetryOnIOExceptions(), retryConfig.isRetryOnIOExceptions()); assertEquals(DEFAULT_RETRY_CONFIG.getRetryStatusCodes(), retryConfig.getRetryStatusCodes()); } } diff --git a/src/test/java/com/google/firebase/messaging/BatchResponseTest.java b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java index 9c174f569..441822220 100644 --- a/src/test/java/com/google/firebase/messaging/BatchResponseTest.java +++ b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; +import com.google.firebase.ErrorCode; import java.util.ArrayList; import java.util.List; import org.junit.Test; @@ -43,8 +44,8 @@ public void testSomeResponse() { ImmutableList responses = ImmutableList.of( SendResponse.fromMessageId("message1"), SendResponse.fromMessageId("message2"), - SendResponse.fromException(new FirebaseMessagingException("error-code", - "error-message", null)) + SendResponse.fromException( + new FirebaseMessagingException(ErrorCode.INTERNAL, "error-message")) ); BatchResponse batchResponse = new BatchResponseImpl(responses); diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java index 0b84e008e..9f8780e89 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -27,6 +27,7 @@ import com.google.api.client.googleapis.util.Utils; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpResponseException; @@ -37,8 +38,10 @@ import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.OutgoingHttpRequest; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.internal.SdkUtils; import com.google.firebase.messaging.WebpushNotification.Action; @@ -62,6 +65,11 @@ public class FirebaseMessagingClientImplTest { private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); + private static final Map HTTP_2_ERROR = ImmutableMap.of( + 401, ErrorCode.UNAUTHENTICATED, + 404, ErrorCode.NOT_FOUND, + 500, ErrorCode.INTERNAL); + private static final String MOCK_RESPONSE = "{\"name\": \"mock-name\"}"; private static final String MOCK_BATCH_SUCCESS_RESPONSE = TestUtils.loadResource( @@ -128,8 +136,8 @@ public void testSendHttpError() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "unknown-error", - "Unexpected HTTP response with status: " + code + "; body: {}"); + checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null, + "Unexpected HTTP response with status: " + code + "\n{}"); } checkRequestHeader(interceptor.getLastRequest()); } @@ -143,9 +151,12 @@ public void testSendTransportError() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling FCM backend service", error.getMessage()); + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertEquals("Unknown error while making a remote service call: transport error", + error.getMessage()); assertTrue(error.getCause() instanceof IOException); + assertNull(error.getHttpResponse()); + assertNull(error.getMessagingErrorCode()); } } @@ -160,8 +171,11 @@ public void testSendSuccessResponseWithUnexpectedPayload() { client.send(entry.getKey(), DRY_RUN_DISABLED); fail("No error thrown for malformed response"); } catch (FirebaseMessagingException error) { - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling FCM backend service", error.getMessage()); + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertTrue(error.getMessage().startsWith("Error while parsing HTTP response: ")); + assertNotNull(error.getCause()); + assertNotNull(error.getHttpResponse()); + assertNull(error.getMessagingErrorCode()); } checkRequestHeader(interceptor.getLastRequest()); } @@ -176,8 +190,8 @@ public void testSendErrorWithZeroContentResponse() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "unknown-error", - "Unexpected HTTP response with status: " + code + "; body: null"); + checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null, + "Unexpected HTTP response with status: " + code + "\nnull"); } checkRequestHeader(interceptor.getLastRequest()); } @@ -192,8 +206,8 @@ public void testSendErrorWithMalformedResponse() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "unknown-error", - "Unexpected HTTP response with status: " + code + "; body: not json"); + checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null, + "Unexpected HTTP response with status: " + code + "\nnot json"); } checkRequestHeader(interceptor.getLastRequest()); } @@ -209,7 +223,7 @@ public void testSendErrorWithDetails() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "invalid-argument"); + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null); } checkRequestHeader(interceptor.getLastRequest()); } @@ -225,7 +239,7 @@ public void testSendErrorWithCanonicalCode() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "registration-token-not-registered"); + checkExceptionFromHttpResponse(error, ErrorCode.NOT_FOUND, null); } checkRequestHeader(interceptor.getLastRequest()); } @@ -243,7 +257,8 @@ public void testSendErrorWithFcmError() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "registration-token-not-registered"); + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, + MessagingErrorCode.UNREGISTERED); } checkRequestHeader(interceptor.getLastRequest()); } @@ -261,7 +276,44 @@ public void testSendErrorWithThirdPartyError() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "third-party-auth-error"); + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, + MessagingErrorCode.THIRD_PARTY_AUTH_ERROR); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendErrorWithUnknownFcmErrorCode() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " + + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + + ".v1.FcmError\", \"errorCode\": \"UNKNOWN_FCM_ERROR\"}]}}"); + + try { + client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendErrorWithDetailsAndNoCode() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " + + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + + ".v1.FcmError\"}]}}"); + + try { + client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null); } checkRequestHeader(interceptor.getLastRequest()); } @@ -338,8 +390,8 @@ public void testSendAllHttpError() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "unknown-error", - "Unexpected HTTP response with status: " + code + "; body: {}"); + checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null, + "Unexpected HTTP response with status: " + code + "\n{}"); } checkBatchRequestHeader(interceptor.getLastRequest()); } @@ -353,9 +405,12 @@ public void testSendAllTransportError() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling FCM backend service", error.getMessage()); + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); assertTrue(error.getCause() instanceof IOException); + assertNull(error.getHttpResponse()); + assertNull(error.getMessagingErrorCode()); } } @@ -368,8 +423,8 @@ public void testSendAllErrorWithEmptyResponse() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "unknown-error", - "Unexpected HTTP response with status: " + code + "; body: null"); + checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null, + "Unexpected HTTP response with status: " + code + "\nnull"); } checkBatchRequestHeader(interceptor.getLastRequest()); } @@ -385,7 +440,7 @@ public void testSendAllErrorWithDetails() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "invalid-argument"); + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null); } checkBatchRequestHeader(interceptor.getLastRequest()); } @@ -401,7 +456,7 @@ public void testSendAllErrorWithCanonicalCode() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "registration-token-not-registered"); + checkExceptionFromHttpResponse(error, ErrorCode.NOT_FOUND, null); } checkBatchRequestHeader(interceptor.getLastRequest()); } @@ -419,7 +474,8 @@ public void testSendAllErrorWithFcmError() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "registration-token-not-registered"); + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, + MessagingErrorCode.UNREGISTERED); } checkBatchRequestHeader(interceptor.getLastRequest()); } @@ -437,8 +493,9 @@ public void testSendAllErrorWithoutMessage() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "registration-token-not-registered", - "Unexpected HTTP response with status: " + code + "; body: " + responseBody); + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, + MessagingErrorCode.UNREGISTERED, + "Unexpected HTTP response with status: " + code + "\n" + responseBody); } checkBatchRequestHeader(interceptor.getLastRequest()); } @@ -466,7 +523,7 @@ public void testBuilderNullChildRequestFactory() { @Test public void testFromApp() throws IOException { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); @@ -476,7 +533,6 @@ public void testFromApp() throws IOException { FirebaseMessagingClientImpl client = FirebaseMessagingClientImpl.fromApp(app); assertEquals(TEST_FCM_URL, client.getFcmSendUrl()); - assertEquals("fire-admin-java/" + SdkUtils.getVersion(), client.getClientVersion()); assertSame(options.getJsonFactory(), client.getJsonFactory()); HttpRequest request = client.getRequestFactory().buildGetRequest( @@ -569,7 +625,10 @@ private void assertBatchResponse( FirebaseMessagingException exception = sendResponse.getException(); assertNotNull(exception); - assertEquals("invalid-argument", exception.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode()); + assertNull(exception.getCause()); + assertNull(exception.getHttpResponse()); + assertEquals(MessagingErrorCode.INVALID_ARGUMENT, exception.getMessagingErrorCode()); } checkBatchRequestHeader(interceptor.getLastRequest()); @@ -610,15 +669,26 @@ private FirebaseMessagingClientImpl.Builder fullyPopulatedBuilder() { } private void checkExceptionFromHttpResponse( - FirebaseMessagingException error, String expectedCode) { - checkExceptionFromHttpResponse(error, expectedCode, "test error"); + FirebaseMessagingException error, + ErrorCode expectedCode, + MessagingErrorCode expectedMessagingCode) { + checkExceptionFromHttpResponse(error, expectedCode, expectedMessagingCode, "test error"); } private void checkExceptionFromHttpResponse( - FirebaseMessagingException error, String expectedCode, String expectedMessage) { + FirebaseMessagingException error, + ErrorCode expectedCode, + MessagingErrorCode expectedMessagingCode, + String expectedMessage) { assertEquals(expectedCode, error.getErrorCode()); assertEquals(expectedMessage, error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); + assertEquals(expectedMessagingCode, error.getMessagingErrorCode()); + + assertNotNull(error.getHttpResponse()); + OutgoingHttpRequest request = error.getHttpResponse().getRequest(); + assertEquals(HttpMethods.POST, request.getMethod()); + assertTrue(request.getUrl().startsWith("https://fcm.googleapis.com")); } private static Map> buildTestMessages() { @@ -633,7 +703,10 @@ private static Map> buildTestMessages() { // Notification message builder.put( Message.builder() - .setNotification(new Notification("test title", "test body")) + .setNotification(Notification.builder() + .setTitle("test title") + .setBody("test body") + .build()) .setTopic("test-topic") .build(), ImmutableMap.of( diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 4d9af55d0..9ae7bba24 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -22,10 +22,13 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import com.google.api.client.http.HttpResponseException; import com.google.common.collect.ImmutableList; +import com.google.firebase.ErrorCode; import com.google.firebase.testing.IntegrationTestUtils; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutionException; import org.junit.BeforeClass; import org.junit.Test; @@ -72,22 +75,52 @@ public void testSend() throws Exception { assertTrue(id != null && id.matches("^projects/.*/messages/.*$")); } + @Test + public void testSendError() throws InterruptedException { + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + Message message = Message.builder() + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) + .setToken("not-a-token") + .build(); + try { + messaging.sendAsync(message, true).get(); + } catch (ExecutionException e) { + FirebaseMessagingException cause = (FirebaseMessagingException) e.getCause(); + assertEquals(ErrorCode.INVALID_ARGUMENT, cause.getErrorCode()); + assertEquals(MessagingErrorCode.INVALID_ARGUMENT, cause.getMessagingErrorCode()); + assertNotNull(cause.getHttpResponse()); + assertTrue(cause.getCause() instanceof HttpResponseException); + } + } + @Test public void testSendAll() throws Exception { List messages = new ArrayList<>(); messages.add( Message.builder() - .setNotification(new Notification("Title", "Body")) + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) .setTopic("foo-bar") .build()); messages.add( Message.builder() - .setNotification(new Notification("Title", "Body")) + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) .setTopic("foo-bar") .build()); messages.add( Message.builder() - .setNotification(new Notification("Title", "Body")) + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) .setToken("not-a-token") .build()); @@ -110,7 +143,7 @@ public void testSendAll() throws Exception { assertNull(responses.get(2).getMessageId()); FirebaseMessagingException exception = responses.get(2).getException(); assertNotNull(exception); - assertEquals("invalid-argument", exception.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode()); } @Test @@ -139,7 +172,10 @@ public void testSendFiveHundred() throws Exception { @Test public void testSendMulticast() throws Exception { MulticastMessage multicastMessage = MulticastMessage.builder() - .setNotification(new Notification("Title", "Body")) + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) .addToken("not-a-token") .addToken("also-not-a-token") .build(); diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 496823175..8e8e79e07 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -27,6 +27,7 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; @@ -50,7 +51,7 @@ public class FirebaseMessagingTest { .addToken("test-fcm-token2") .build(); private static final FirebaseMessagingException TEST_EXCEPTION = - new FirebaseMessagingException("TEST_CODE", "Test error message", new Exception()); + new FirebaseMessagingException(ErrorCode.INTERNAL, "Test error message"); private static final ImmutableList.Builder TOO_MANY_IDS = ImmutableList.builder(); diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java index c7e1cc24a..7724e4f6e 100644 --- a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java @@ -17,12 +17,13 @@ package com.google.firebase.messaging; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.api.client.googleapis.util.Utils; -import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; @@ -31,8 +32,12 @@ import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.OutgoingHttpRequest; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.testing.TestResponseInterceptor; @@ -55,6 +60,11 @@ public class InstanceIdClientImplTest { private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); + private static final Map HTTP_2_ERROR = ImmutableMap.of( + 401, ErrorCode.UNAUTHENTICATED, + 404, ErrorCode.NOT_FOUND, + 500, ErrorCode.INTERNAL); + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -98,14 +108,16 @@ public void testSubscribeError() { TestResponseInterceptor interceptor = new TestResponseInterceptor(); InstanceIdClient client = initInstanceIdClient(response, interceptor); + String content = "{\"error\": \"ErrorCode\"}"; for (int statusCode : HTTP_ERRORS) { - response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); + response.setStatusCode(statusCode).setContent(content); try { client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, statusCode, "test error"); + String expectedMessage = "Error while calling the IID service: ErrorCode"; + checkExceptionFromHttpResponse(error, statusCode, expectedMessage); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); @@ -124,7 +136,7 @@ public void testSubscribeEmptyPayloadError() { fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { checkExceptionFromHttpResponse(error, 500, - "Unexpected HTTP response with status: 500; body: {}"); + "Unexpected HTTP response with status: 500\n{}"); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); @@ -142,7 +154,7 @@ public void testSubscribeMalformedError() { fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { checkExceptionFromHttpResponse(error, 500, - "Unexpected HTTP response with status: 500; body: not json"); + "Unexpected HTTP response with status: 500\nnot json"); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); @@ -160,7 +172,7 @@ public void testSubscribeZeroContentError() { fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { checkExceptionFromHttpResponse(error, 500, - "Unexpected HTTP response with status: 500; body: null"); + "Unexpected HTTP response with status: 500\nnull"); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); @@ -174,8 +186,26 @@ public void testSubscribeTransportError() { client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling IID backend service", error.getMessage()); + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test + public void testSubscribeParseError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("not json"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + + try { + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertTrue(error.getMessage().startsWith("Error while parsing HTTP response: ")); assertTrue(error.getCause() instanceof IOException); } } @@ -218,14 +248,16 @@ public void testUnsubscribeError() { TestResponseInterceptor interceptor = new TestResponseInterceptor(); InstanceIdClient client = initInstanceIdClient(response, interceptor); + String content = "{\"error\": \"ErrorCode\"}"; for (int statusCode : HTTP_ERRORS) { - response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); + response.setStatusCode(statusCode).setContent(content); try { client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, statusCode, "test error"); + String expectedMessage = "Error while calling the IID service: ErrorCode"; + checkExceptionFromHttpResponse(error, statusCode, expectedMessage); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); @@ -244,7 +276,7 @@ public void testUnsubscribeEmptyPayloadError() { fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { checkExceptionFromHttpResponse(error, 500, - "Unexpected HTTP response with status: 500; body: {}"); + "Unexpected HTTP response with status: 500\n{}"); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); @@ -262,7 +294,7 @@ public void testUnsubscribeMalformedError() { fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { checkExceptionFromHttpResponse(error, 500, - "Unexpected HTTP response with status: 500; body: not json"); + "Unexpected HTTP response with status: 500\nnot json"); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); @@ -280,7 +312,7 @@ public void testUnsubscribeZeroContentError() { fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { checkExceptionFromHttpResponse(error, 500, - "Unexpected HTTP response with status: 500; body: null"); + "Unexpected HTTP response with status: 500\nnull"); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); @@ -294,8 +326,26 @@ public void testUnsubscribeTransportError() { client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling IID backend service", error.getMessage()); + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test + public void testUnsubscribeParseError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("not json"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + + try { + client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertTrue(error.getMessage().startsWith("Error while parsing HTTP response: ")); assertTrue(error.getCause() instanceof IOException); } } @@ -311,9 +361,15 @@ public void testJsonFactoryIsNull() { } @Test - public void testFromApp() throws IOException { - FirebaseOptions options = new FirebaseOptions.Builder() + public void testFromApp() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(400).setZeroContent(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) + .setHttpTransport(transport) .setProjectId("test-project") .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -321,10 +377,14 @@ public void testFromApp() throws IOException { try { InstanceIdClientImpl client = InstanceIdClientImpl.fromApp(app); - assertSame(options.getJsonFactory(), client.getJsonFactory()); - HttpRequest request = client.getRequestFactory().buildGetRequest( - new GenericUrl("https://example.com")); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for error response"); + } catch (FirebaseMessagingException e) { + assertNotNull(e.getHttpResponse()); + + List auth = ImmutableList.of("Bearer test-token"); + OutgoingHttpRequest request = e.getHttpResponse().getRequest(); + assertEquals(auth, request.getHeaders().get("authorization")); } finally { app.delete(); } @@ -388,11 +448,20 @@ private void checkTopicManagementRequestHeader( assertEquals(expectedUrl, request.getUrl().toString()); } - private void checkExceptionFromHttpResponse(FirebaseMessagingException error, - int expectedCode, String expectedMessage) { - assertEquals(getTopicManagementErrorCode(expectedCode), error.getErrorCode()); + private void checkExceptionFromHttpResponse( + FirebaseMessagingException error, int statusCode, String expectedMessage) { + assertEquals(HTTP_2_ERROR.get(statusCode), error.getErrorCode()); assertEquals(expectedMessage, error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); + assertNull(error.getMessagingErrorCode()); + + IncomingHttpResponse httpResponse = error.getHttpResponse(); + assertNotNull(httpResponse); + assertEquals(statusCode, httpResponse.getStatusCode()); + + OutgoingHttpRequest request = httpResponse.getRequest(); + assertEquals(HttpMethods.POST, request.getMethod()); + assertTrue(request.getUrl().startsWith("https://iid.googleapis.com")); } private InstanceIdClient initClientWithFaultyTransport() { @@ -400,12 +469,4 @@ private InstanceIdClient initClientWithFaultyTransport() { TestUtils.createFaultyHttpTransport().createRequestFactory(), Utils.getDefaultJsonFactory()); } - - private String getTopicManagementErrorCode(int statusCode) { - String code = InstanceIdClientImpl.IID_ERROR_CODES.get(statusCode); - if (code == null) { - code = "unknown-error"; - } - return code; - } } diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index c97fb6b67..90b867d5f 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -29,11 +29,8 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; import org.junit.Test; @@ -94,7 +91,10 @@ public void testPrefixedTopicName() throws IOException { @Test public void testNotificationMessageDeprecatedConstructor() throws IOException { Message message = Message.builder() - .setNotification(new Notification("title", "body")) + .setNotification(Notification.builder() + .setTitle("title") + .setBody("body") + .build()) .setTopic("test-topic") .build(); Map data = ImmutableMap.of("title", "title", "body", "body"); @@ -752,7 +752,11 @@ public void testIncorrectAnalyticsLabelFormat() { @Test public void testImageInNotificationDeprecatedConstructor() throws IOException { Message message = Message.builder() - .setNotification(new Notification("title", "body", TEST_IMAGE_URL)) + .setNotification(Notification.builder() + .setTitle("title") + .setBody("body") + .setImage(TEST_IMAGE_URL) + .build()) .setTopic("test-topic") .build(); Map data = ImmutableMap.of( @@ -778,7 +782,11 @@ public void testImageInNotification() throws IOException { @Test public void testImageInAndroidNotification() throws IOException { Message message = Message.builder() - .setNotification(new Notification("title", "body", TEST_IMAGE_URL)) + .setNotification(Notification.builder() + .setTitle("title") + .setBody("body") + .setImage(TEST_IMAGE_URL) + .build()) .setAndroidConfig(AndroidConfig.builder() .setNotification(AndroidNotification.builder() .setTitle("android-title") @@ -808,7 +816,11 @@ public void testImageInAndroidNotification() throws IOException { public void testImageInApnsNotification() throws IOException { Message message = Message.builder() .setTopic("test-topic") - .setNotification(new Notification("title", "body", TEST_IMAGE_URL)) + .setNotification(Notification.builder() + .setTitle("title") + .setBody("body") + .setImage(TEST_IMAGE_URL) + .build()) .setApnsConfig( ApnsConfig.builder().setAps(Aps.builder().build()) .setFcmOptions(ApnsFcmOptions.builder().setImage(TEST_IMAGE_URL_APNS).build()) @@ -835,7 +847,7 @@ public void testImageInApnsNotification() throws IOException { } @Test - public void testInvalidColorInAndroidNotificationLightSettings() throws IOException { + public void testInvalidColorInAndroidNotificationLightSettings() { try { LightSettings.Builder lightSettingsBuilder = LightSettings.builder() .setColorFromString("#01020K") @@ -853,7 +865,10 @@ public void testInvalidColorInAndroidNotificationLightSettings() throws IOExcept public void testExtendedAndroidNotificationParameters() throws IOException { long[] vibrateTimings = {1000L, 1001L}; Message message = Message.builder() - .setNotification(new Notification("title", "body")) + .setNotification(Notification.builder() + .setTitle("title") + .setBody("body") + .build()) .setAndroidConfig(AndroidConfig.builder() .setNotification(AndroidNotification.builder() .setTitle("android-title") diff --git a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java index 1ea57d8d5..f26d0816f 100644 --- a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java @@ -38,7 +38,10 @@ public class MulticastMessageTest { private static final WebpushConfig WEBPUSH = WebpushConfig.builder() .putData("key", "value") .build(); - private static final Notification NOTIFICATION = new Notification("title", "body"); + private static final Notification NOTIFICATION = Notification.builder() + .setTitle("title") + .setBody("body") + .build(); private static final FcmOptions FCM_OPTIONS = FcmOptions.withAnalyticsLabel("analytics_label"); @Test diff --git a/src/test/java/com/google/firebase/messaging/SendResponseTest.java b/src/test/java/com/google/firebase/messaging/SendResponseTest.java index e9667cc77..d90f1e6b6 100644 --- a/src/test/java/com/google/firebase/messaging/SendResponseTest.java +++ b/src/test/java/com/google/firebase/messaging/SendResponseTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import com.google.firebase.ErrorCode; import org.junit.Test; public class SendResponseTest { @@ -37,8 +38,8 @@ public void testSuccessfulResponse() { @Test public void testFailureResponse() { - FirebaseMessagingException exception = new FirebaseMessagingException("error-code", - "error-message", null); + FirebaseMessagingException exception = new FirebaseMessagingException( + ErrorCode.INTERNAL, "error-message"); SendResponse response = SendResponse.fromException(exception); assertNull(response.getMessageId()); diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java index 87afe609c..8d227ef52 100644 --- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java @@ -18,12 +18,11 @@ 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.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -40,12 +39,15 @@ import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.SdkUtils; import com.google.firebase.internal.TestApiClientUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; +import com.google.firebase.testing.TestUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; @@ -69,6 +71,7 @@ public class FirebaseProjectManagementServiceImplTest { 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 String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion(); 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 = @@ -272,7 +275,65 @@ public void getIosAppHttpError() { 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()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "App ID \"test-ios-app-id\": Unexpected HTTP response with status: 500\n{}", + e.getMessage()); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void getIosAppHttpErrorWithCode() { + firstRpcResponse.setStatusCode(500); + firstRpcResponse.setContent( + "{\"error\": {\"status\":\"NOT_FOUND\", \"message\":\"Test error\"}}"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + try { + serviceImpl.getIosApp(IOS_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals("App ID \"test-ios-app-id\": Test error", e.getMessage()); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void getIosAppParseError() { + firstRpcResponse.setContent("not json"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + try { + serviceImpl.getIosApp(IOS_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertTrue(e.getMessage().startsWith( + "App ID \"test-ios-app-id\": Error while parsing HTTP response")); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void getIosAppTransportError() { + FirebaseProjectManagementServiceImpl serviceImpl = initServiceImplWithFaultyTransport(); + + try { + serviceImpl.getIosApp(IOS_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals( + "App ID \"test-ios-app-id\": Unknown error while making a remote service call: " + + "transport error", + e.getMessage()); + assertNotNull(e.getCause()); + assertNull(e.getHttpResponse()); } } @@ -467,11 +528,10 @@ public void setIosDisplayName() throws Exception { "%s/v1beta1/projects/-/iosApps/%s?update_mask=display_name", FIREBASE_PROJECT_MANAGEMENT_URL, IOS_APP_ID); - checkRequestHeader(expectedUrl, HttpMethod.POST); + checkRequestHeader(expectedUrl, HttpMethod.PATCH); ImmutableMap payload = ImmutableMap.builder().put("display_name", DISPLAY_NAME).build(); checkRequestPayload(payload); - checkPatchRequest(); } @Test @@ -485,11 +545,10 @@ public void setIosDisplayNameAsync() throws Exception { "%s/v1beta1/projects/-/iosApps/%s?update_mask=display_name", FIREBASE_PROJECT_MANAGEMENT_URL, IOS_APP_ID); - checkRequestHeader(expectedUrl, HttpMethod.POST); + checkRequestHeader(expectedUrl, HttpMethod.PATCH); ImmutableMap payload = ImmutableMap.builder().put("display_name", DISPLAY_NAME).build(); checkRequestPayload(payload); - checkPatchRequest(); } @Test @@ -554,7 +613,65 @@ public void getAndroidAppHttpError() { 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()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "App ID \"test-android-app-id\": Unexpected HTTP response with status: 500\n{}", + e.getMessage()); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void getAndroidAppHttpErrorWithCode() { + firstRpcResponse.setStatusCode(500); + firstRpcResponse.setContent( + "{\"error\": {\"status\":\"NOT_FOUND\", \"message\":\"Test error\"}}"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + try { + serviceImpl.getAndroidApp(ANDROID_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals("App ID \"test-android-app-id\": Test error", e.getMessage()); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void getAndroidAppParseError() { + firstRpcResponse.setContent("not json"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + try { + serviceImpl.getAndroidApp(ANDROID_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertTrue(e.getMessage().startsWith( + "App ID \"test-android-app-id\": Error while parsing HTTP response")); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void getAndroidAppTransportError() { + FirebaseProjectManagementServiceImpl serviceImpl = initServiceImplWithFaultyTransport(); + + try { + serviceImpl.getAndroidApp(ANDROID_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals( + "App ID \"test-android-app-id\": Unknown error while making a remote service call: " + + "transport error", + e.getMessage()); + assertNotNull(e.getCause()); + assertNull(e.getHttpResponse()); } } @@ -751,11 +868,10 @@ public void setAndroidDisplayName() throws Exception { "%s/v1beta1/projects/-/androidApps/%s?update_mask=display_name", FIREBASE_PROJECT_MANAGEMENT_URL, ANDROID_APP_ID); - checkRequestHeader(expectedUrl, HttpMethod.POST); + checkRequestHeader(expectedUrl, HttpMethod.PATCH); ImmutableMap payload = ImmutableMap.builder().put("display_name", DISPLAY_NAME).build(); checkRequestPayload(payload); - checkPatchRequest(); } @Test @@ -769,11 +885,10 @@ public void setAndroidDisplayNameAsync() throws Exception { "%s/v1beta1/projects/-/androidApps/%s?update_mask=display_name", FIREBASE_PROJECT_MANAGEMENT_URL, ANDROID_APP_ID); - checkRequestHeader(expectedUrl, HttpMethod.POST); + checkRequestHeader(expectedUrl, HttpMethod.PATCH); ImmutableMap payload = ImmutableMap.builder().put("display_name", DISPLAY_NAME).build(); checkRequestPayload(payload); - checkPatchRequest(); } @Test @@ -918,17 +1033,25 @@ public void deleteShaCertificateAsync() throws Exception { } @Test - public void testAuthAndRetriesSupport() { - FirebaseOptions options = new FirebaseOptions.Builder() + public void testAuthAndRetriesSupport() throws Exception { + List mockResponses = ImmutableList.of( + new MockLowLevelHttpResponse().setContent("{}")); + MockHttpTransport transport = new MultiRequestMockHttpTransport(mockResponses); + FirebaseOptions options = 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); + + serviceImpl.deleteShaCertificate(SHA1_RESOURCE_NAME); - TestApiClientUtils.assertAuthAndRetrySupport(serviceImpl.getRequestFactory()); + assertEquals(1, interceptor.getNumberOfResponses()); + TestApiClientUtils.assertAuthAndRetrySupport(interceptor.getResponse(0).getRequest()); } @Test @@ -937,7 +1060,7 @@ public void testHttpRetries() throws Exception { firstRpcResponse.setStatusCode(503).setContent("{}"), new MockLowLevelHttpResponse().setContent("{}")); MockHttpTransport transport = new MultiRequestMockHttpTransport(mockResponses); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId(PROJECT_ID) .setHttpTransport(transport) @@ -965,7 +1088,7 @@ private static FirebaseProjectManagementServiceImpl initServiceImpl( List mockResponses, MultiRequestTestResponseInterceptor interceptor) { MockHttpTransport transport = new MultiRequestMockHttpTransport(mockResponses); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId(PROJECT_ID) .setHttpTransport(transport) @@ -978,6 +1101,16 @@ private static FirebaseProjectManagementServiceImpl initServiceImpl( return serviceImpl; } + private static FirebaseProjectManagementServiceImpl initServiceImplWithFaultyTransport() { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId(PROJECT_ID) + .setHttpTransport(TestUtils.createFaultyHttpTransport()) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + return new FirebaseProjectManagementServiceImpl(app); + } + private void checkRequestHeader(String expectedUrl, HttpMethod httpMethod) { assertEquals( "The number of HttpResponses is not equal to 1.", 1, interceptor.getNumberOfResponses()); @@ -990,6 +1123,7 @@ private void checkRequestHeader(int index, String expectedUrl, HttpMethod httpMe assertEquals(httpMethod.name(), request.getRequestMethod()); assertEquals(expectedUrl, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + assertEquals(CLIENT_VERSION, request.getHeaders().get("X-Client-Version")); } private void checkRequestPayload(Map expected) throws IOException { @@ -1007,21 +1141,11 @@ private void checkRequestPayload(int index, Map expected) throws 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 + DELETE, + PATCH, } /** diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java index 7569b0bc2..30fcc3344 100644 --- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java @@ -26,6 +26,7 @@ import com.google.api.core.ApiFutures; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; @@ -52,7 +53,7 @@ public class FirebaseProjectManagementTest { 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); + new FirebaseProjectManagementException(ErrorCode.UNKNOWN, "Error!", null); @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -67,7 +68,7 @@ public class FirebaseProjectManagementTest { @BeforeClass public static void setUpClass() { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId(TEST_PROJECT_ID) .build(); diff --git a/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java b/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java index 269d3dae0..02d50279b 100644 --- a/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java @@ -26,6 +26,7 @@ import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; +import com.google.firebase.ErrorCode; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import java.util.concurrent.ExecutionException; import org.junit.After; @@ -53,7 +54,7 @@ public class IosAppTest { + "SOME_OTHER_KEYsome-other-value" + ""; private static final FirebaseProjectManagementException FIREBASE_PROJECT_MANAGEMENT_EXCEPTION = - new FirebaseProjectManagementException("Error!", null); + new FirebaseProjectManagementException(ErrorCode.UNKNOWN, "Error!", null); private static final IosAppMetadata TEST_IOS_APP_METADATA = new IosAppMetadata( TEST_APP_NAME, diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java index ed1ad464e..bf3d4ca6b 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java @@ -33,7 +33,7 @@ public void initializeWithServiceAccount() throws IOException { // [START initialize_sdk_with_service_account] FileInputStream serviceAccount = new FileInputStream("path/to/serviceAccountKey.json"); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(serviceAccount)) .setDatabaseUrl("https://.firebaseio.com/") .build(); @@ -44,7 +44,7 @@ public void initializeWithServiceAccount() throws IOException { public void initializeWithDefaultCredentials() throws IOException { // [START initialize_sdk_with_application_default] - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .setDatabaseUrl("https://.firebaseio.com/") .build(); @@ -57,7 +57,7 @@ public void initializeWithRefreshToken() throws IOException { // [START initialize_sdk_with_refresh_token] FileInputStream refreshToken = new FileInputStream("path/to/refreshToken.json"); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(refreshToken)) .setDatabaseUrl("https://.firebaseio.com/") .build(); @@ -73,7 +73,7 @@ public void initializeWithDefaultConfig() { } public void initializeDefaultApp() throws IOException { - FirebaseOptions defaultOptions = new FirebaseOptions.Builder() + FirebaseOptions defaultOptions = FirebaseOptions.builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .build(); @@ -94,10 +94,10 @@ public void initializeDefaultApp() throws IOException { } public void initializeCustomApp() throws Exception { - FirebaseOptions defaultOptions = new FirebaseOptions.Builder() + FirebaseOptions defaultOptions = FirebaseOptions.builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .build(); - FirebaseOptions otherAppConfig = new FirebaseOptions.Builder() + FirebaseOptions otherAppConfig = FirebaseOptions.builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .build(); @@ -123,7 +123,7 @@ public void initializeCustomApp() throws Exception { public void initializeWithServiceAccountId() throws IOException { // [START initialize_sdk_with_service_account_id] - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .setServiceAccountId("my-client-id@my-project-id.iam.gserviceaccount.com") .build(); diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java index b6103b4ef..e86496a21 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java @@ -18,6 +18,7 @@ import com.google.common.io.BaseEncoding; import com.google.firebase.auth.ActionCodeSettings; +import com.google.firebase.auth.AuthErrorCode; import com.google.firebase.auth.ErrorInfo; import com.google.firebase.auth.ExportedUserRecord; import com.google.firebase.auth.FirebaseAuth; @@ -262,7 +263,7 @@ public static void verifyIdTokenCheckRevoked(String idToken) { // Token is valid and not revoked. String uid = decodedToken.getUid(); } catch (FirebaseAuthException e) { - if (e.getErrorCode().equals("id-token-revoked")) { + if (e.getAuthErrorCode() == AuthErrorCode.REVOKED_ID_TOKEN) { // Token has been revoked. Inform the user to re-authenticate or signOut() the user. } else { // Token is invalid. diff --git a/src/test/java/com/google/firebase/snippets/FirebaseDatabaseSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseDatabaseSnippets.java index baf4d3b69..079946649 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseDatabaseSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseDatabaseSnippets.java @@ -677,7 +677,7 @@ public void initializeApp() throws IOException { FileInputStream serviceAccount = new FileInputStream("path/to/serviceAccount.json"); // Initialize the app with a service account, granting admin privileges - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(serviceAccount)) .setDatabaseUrl("https://.firebaseio.com") .build(); diff --git a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java index d6e4417e1..f1432e160 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java @@ -88,9 +88,10 @@ public void sendToCondition() throws FirebaseMessagingException { // See documentation on defining a message payload. Message message = Message.builder() - .setNotification(new Notification( - "$GOOG up 1.43% on the day", - "$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.")) + .setNotification(Notification.builder() + .setTitle("$GOOG up 1.43% on the day") + .setBody("$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.") + .build()) .setCondition(condition) .build(); @@ -125,12 +126,18 @@ public void sendAll() throws FirebaseMessagingException { // Create a list containing up to 500 messages. List messages = Arrays.asList( Message.builder() - .setNotification(new Notification("Price drop", "5% off all electronics")) + .setNotification(Notification.builder() + .setTitle("Price drop") + .setBody("5% off all electronics") + .build()) .setToken(registrationToken) .build(), // ... Message.builder() - .setNotification(new Notification("Price drop", "2% off all books")) + .setNotification(Notification.builder() + .setTitle("Price drop") + .setBody("2% off all books") + .build()) .setTopic("readers-club") .build() ); @@ -251,9 +258,10 @@ public Message webpushMessage() { public Message allPlatformsMessage() { // [START multi_platforms_message] Message message = Message.builder() - .setNotification(new Notification( - "$GOOG up 1.43% on the day", - "$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.")) + .setNotification(Notification.builder() + .setTitle("$GOOG up 1.43% on the day") + .setBody("$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.") + .build()) .setAndroidConfig(AndroidConfig.builder() .setTtl(3600 * 1000) .setNotification(AndroidNotification.builder() diff --git a/src/test/java/com/google/firebase/snippets/FirebaseStorageSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseStorageSnippets.java index 380dcff1b..706cc1b16 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseStorageSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseStorageSnippets.java @@ -33,7 +33,7 @@ public void initializeAppForStorage() throws IOException { // [START init_admin_sdk_for_storage] FileInputStream serviceAccount = new FileInputStream("path/to/serviceAccountKey.json"); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(serviceAccount)) .setStorageBucket(".appspot.com") .build(); diff --git a/src/test/java/com/google/firebase/testing/FirebaseAppRule.java b/src/test/java/com/google/firebase/testing/FirebaseAppRule.java index 28123293f..1049a1f2f 100644 --- a/src/test/java/com/google/firebase/testing/FirebaseAppRule.java +++ b/src/test/java/com/google/firebase/testing/FirebaseAppRule.java @@ -17,7 +17,6 @@ package com.google.firebase.testing; import com.google.firebase.TestOnlyImplFirebaseTrampolines; -import com.google.firebase.internal.FirebaseAppStore; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -42,6 +41,5 @@ public void evaluate() throws Throwable { private void resetState() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); - FirebaseAppStore.clearInstanceForTest(); } } diff --git a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java index d3abaa096..c9890354e 100644 --- a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java +++ b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java @@ -110,7 +110,6 @@ public static synchronized FirebaseApp ensureDefaultApp() { .setStorageBucket(getStorageBucket()) .setCredentials(TestUtils.getCertCredential(getServiceAccountCertificate())) .setFirestoreOptions(FirestoreOptions.newBuilder() - .setTimestampsInSnapshotsEnabled(true) .setCredentials(TestUtils.getCertCredential(getServiceAccountCertificate())) .build()) .build(); @@ -121,7 +120,7 @@ public static synchronized FirebaseApp ensureDefaultApp() { public static FirebaseApp initApp(String name) { FirebaseOptions options = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setDatabaseUrl(getDatabaseUrl()) .setCredentials(TestUtils.getCertCredential(getServiceAccountCertificate())) .build(); diff --git a/src/test/java/com/google/firebase/testing/TestUtils.java b/src/test/java/com/google/firebase/testing/TestUtils.java index a63aa8d10..e44ca2be8 100644 --- a/src/test/java/com/google/firebase/testing/TestUtils.java +++ b/src/test/java/com/google/firebase/testing/TestUtils.java @@ -162,11 +162,19 @@ public static HttpRequest createRequest() throws IOException { } public static HttpRequest createRequest(MockLowLevelHttpRequest request) throws IOException { + return createRequest(request, TEST_URL); + } + + /** + * Creates a test HTTP POST request for the given target URL. + */ + public static HttpRequest createRequest( + MockLowLevelHttpRequest request, GenericUrl url) throws IOException { HttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpRequest(request) .build(); HttpRequestFactory requestFactory = transport.createRequestFactory(); - return requestFactory.buildPostRequest(TEST_URL, new EmptyContent()); + return requestFactory.buildPostRequest(url, new EmptyContent()); } public static HttpTransport createFaultyHttpTransport() { From 47d43475d04ea6777c7c04dd15c1ddf379e21cd8 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 19 Aug 2020 10:47:28 -0700 Subject: [PATCH 131/456] [chore] Release 7.0.0 (#473) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a72301d5b..0f40ea078 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.16.0 + 7.0.0 jar firebase-admin From 062223d527c9f30aaca033a3b1dfe174f97f07b2 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 31 Aug 2020 10:51:26 -0700 Subject: [PATCH 132/456] fix(auth): Support verifying tenant ID tokens in FirebaseAuth (#475) * fix(auth): Support verifying tenant ID tokens in FirebaseAuth * fix: Fixing the error message for null tenant ID --- .../auth/FirebaseTokenVerifierImpl.java | 8 ++-- .../com/google/firebase/FirebaseAppTest.java | 26 ++++++------- .../auth/FirebaseTokenVerifierImplTest.java | 38 +++++++++++++++++-- .../TenantAwareFirebaseAuthIT.java | 5 +++ 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java index 273ec1532..bbe741f5b 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java @@ -71,7 +71,7 @@ private FirebaseTokenVerifierImpl(Builder builder) { this.docUrl = builder.docUrl; this.invalidTokenErrorCode = checkNotNull(builder.invalidTokenErrorCode); this.expiredTokenErrorCode = checkNotNull(builder.expiredTokenErrorCode); - this.tenantId = Strings.nullToEmpty(builder.tenantId); + this.tenantId = builder.tenantId; } /** @@ -323,11 +323,11 @@ private boolean containsLegacyUidField(IdToken.Payload payload) { } private void checkTenantId(final FirebaseToken firebaseToken) throws FirebaseAuthException { - String tokenTenantId = Strings.nullToEmpty(firebaseToken.getTenantId()); - if (!this.tenantId.equals(tokenTenantId)) { + String tokenTenantId = firebaseToken.getTenantId(); + if (this.tenantId != null && !this.tenantId.equals(tokenTenantId)) { String message = String.format( "The tenant ID ('%s') of the token did not match the expected value ('%s')", - tokenTenantId, + Strings.nullToEmpty(tokenTenantId), tenantId); throw newException(message, AuthErrorCode.TENANT_ID_MISMATCH); } diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java index d481e3c0e..26ca93432 100644 --- a/src/test/java/com/google/firebase/FirebaseAppTest.java +++ b/src/test/java/com/google/firebase/FirebaseAppTest.java @@ -66,7 +66,7 @@ import org.junit.Test; import org.mockito.Mockito; -/** +/** * Unit tests for {@link com.google.firebase.FirebaseApp}. */ public class FirebaseAppTest { @@ -472,8 +472,8 @@ public void testAppWithAuthVariableOverrides() { public void testEmptyFirebaseConfigFile() { setFirebaseConfigEnvironmentVariable("firebase_config_empty.json"); FirebaseApp.initializeApp(); - } - + } + @Test public void testEmptyFirebaseConfigString() { setFirebaseConfigEnvironmentVariable(""); @@ -481,7 +481,7 @@ public void testEmptyFirebaseConfigString() { assertNull(firebaseApp.getOptions().getProjectId()); assertNull(firebaseApp.getOptions().getStorageBucket()); assertNull(firebaseApp.getOptions().getDatabaseUrl()); - assertTrue(firebaseApp.getOptions().getDatabaseAuthVariableOverride().isEmpty()); + assertTrue(firebaseApp.getOptions().getDatabaseAuthVariableOverride().isEmpty()); } @Test @@ -542,20 +542,20 @@ public void testEnvironmentVariableIgnored() { @Test public void testValidFirebaseConfigString() { - setFirebaseConfigEnvironmentVariable("{" - + "\"databaseAuthVariableOverride\": {" - + "\"uid\":" - + "\"testuser\"" - + "}," - + "\"databaseUrl\": \"https://hipster-chat.firebaseio.mock\"," - + "\"projectId\": \"hipster-chat-mock\"," - + "\"storageBucket\": \"hipster-chat.appspot.mock\"" + setFirebaseConfigEnvironmentVariable("{" + + "\"databaseAuthVariableOverride\": {" + + "\"uid\":" + + "\"testuser\"" + + "}," + + "\"databaseUrl\": \"https://hipster-chat.firebaseio.mock\"," + + "\"projectId\": \"hipster-chat-mock\"," + + "\"storageBucket\": \"hipster-chat.appspot.mock\"" + "}"); FirebaseApp firebaseApp = FirebaseApp.initializeApp(); assertEquals("hipster-chat-mock", firebaseApp.getOptions().getProjectId()); assertEquals("hipster-chat.appspot.mock", firebaseApp.getOptions().getStorageBucket()); assertEquals("https://hipster-chat.firebaseio.mock", firebaseApp.getOptions().getDatabaseUrl()); - assertEquals("testuser", + assertEquals("testuser", firebaseApp.getOptions().getDatabaseAuthVariableOverride().get("uid")); } diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java index 833e2ed95..189cb223e 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -328,6 +328,17 @@ public void testMalformedToken() { @Test public void testVerifyTokenWithTenantId() throws FirebaseAuthException { + FirebaseTokenVerifierImpl verifier = fullyPopulatedBuilder().build(); + + FirebaseToken firebaseToken = verifier.verifyToken(createTokenWithTenantId("TENANT_1")); + + assertEquals(TEST_TOKEN_ISSUER, firebaseToken.getIssuer()); + assertEquals(TestTokenFactory.UID, firebaseToken.getUid()); + assertEquals("TENANT_1", firebaseToken.getTenantId()); + } + + @Test + public void testVerifyTokenWithMatchingTenantId() throws FirebaseAuthException { FirebaseTokenVerifierImpl verifier = fullyPopulatedBuilder() .setTenantId("TENANT_1") .build(); @@ -341,11 +352,13 @@ public void testVerifyTokenWithTenantId() throws FirebaseAuthException { @Test public void testVerifyTokenDifferentTenantIds() { - try { - fullyPopulatedBuilder() + FirebaseTokenVerifierImpl verifier = fullyPopulatedBuilder() .setTenantId("TENANT_1") - .build() - .verifyToken(createTokenWithTenantId("TENANT_2")); + .build(); + String token = createTokenWithTenantId("TENANT_2"); + + try { + verifier.verifyToken(token); } catch (FirebaseAuthException e) { assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode()); assertEquals( @@ -354,6 +367,23 @@ public void testVerifyTokenDifferentTenantIds() { } } + @Test + public void testVerifyTokenNoTenantId() { + FirebaseTokenVerifierImpl verifier = fullyPopulatedBuilder() + .setTenantId("TENANT_1") + .build(); + String token = tokenFactory.createToken(); + + try { + verifier.verifyToken(token); + } catch (FirebaseAuthException e) { + assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode()); + assertEquals( + "The tenant ID ('') of the token did not match the expected value ('TENANT_1')", + e.getMessage()); + } + } + @Test public void testVerifyTokenMissingTenantId() { try { diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java index fd8e4af83..1ebac213f 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java @@ -261,6 +261,11 @@ public void testVerifyTokenWithWrongTenantAwareClient() throws Exception { assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, ((FirebaseAuthException) e.getCause()).getAuthErrorCode()); } + + // Verifies with FirebaseAuth + FirebaseToken decoded = FirebaseAuth.getInstance().verifyIdToken(idToken); + assertEquals("user", decoded.getUid()); + assertEquals(tenantId, decoded.getTenantId()); } @Test From 0b356e1fdf6da40f01315f54dad0322a0d732803 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 10 Sep 2020 14:27:15 -0700 Subject: [PATCH 133/456] fix: Cleaning up FirebaseApp state management (#476) --- .../java/com/google/firebase/FirebaseApp.java | 8 ++- .../firebase/ImplFirebaseTrampolines.java | 1 - .../firebase/auth/AbstractFirebaseAuth.java | 48 ---------------- .../google/firebase/auth/FirebaseAuth.java | 8 --- .../multitenancy/TenantAwareFirebaseAuth.java | 5 -- .../google/firebase/cloud/StorageClient.java | 8 --- .../firebase/database/FirebaseDatabase.java | 6 +- .../firebase/iid/FirebaseInstanceId.java | 7 --- .../firebase/internal/FirebaseService.java | 6 +- .../firebase/messaging/FirebaseMessaging.java | 7 --- .../FirebaseProjectManagement.java | 5 -- .../FirebaseProjectManagementServiceImpl.java | 8 --- .../com/google/firebase/FirebaseAppTest.java | 34 +++++++++++ .../firebase/auth/FirebaseAuthTest.java | 57 +++++++++---------- .../firebase/iid/FirebaseInstanceIdTest.java | 35 ++++++++---- 15 files changed, 100 insertions(+), 143 deletions(-) diff --git a/src/main/java/com/google/firebase/FirebaseApp.java b/src/main/java/com/google/firebase/FirebaseApp.java index 38482541c..06188cd55 100644 --- a/src/main/java/com/google/firebase/FirebaseApp.java +++ b/src/main/java/com/google/firebase/FirebaseApp.java @@ -277,6 +277,8 @@ public FirebaseOptions getOptions() { */ @Nullable String getProjectId() { + checkNotDeleted(); + // Try to get project ID from user-specified options. String projectId = options.getProjectId(); @@ -314,8 +316,10 @@ public String toString() { } /** - * Deletes the {@link FirebaseApp} and all its data. All calls to this {@link FirebaseApp} - * instance will throw once it has been called. + * Deletes this {@link FirebaseApp} object, and releases any local state and managed resources + * associated with it. All calls to this {@link FirebaseApp} instance will throw once this method + * has been called. This also releases any managed resources allocated by other services + * attached to this object instance (e.g. {@code FirebaseAuth}). * *

      A no-op if delete was called before. */ diff --git a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java index 7a50e0b07..3a7f6499a 100644 --- a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java +++ b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java @@ -26,7 +26,6 @@ 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; diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 8004548a5..fc9c8df0b 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; import com.google.api.client.json.JsonFactory; import com.google.api.client.util.Clock; @@ -164,7 +163,6 @@ public ApiFuture createCustomTokenAsync( private CallableOperation createCustomTokenOp( final String uid, final Map developerClaims) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); final FirebaseTokenFactory tokenFactory = this.tokenFactory.get(); return new CallableOperation() { @@ -208,7 +206,6 @@ public ApiFuture createSessionCookieAsync( private CallableOperation createSessionCookieOp( final String idToken, final SessionCookieOptions options) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty"); checkNotNull(options, "options must not be null"); final FirebaseUserManager userManager = getUserManager(); @@ -299,7 +296,6 @@ public ApiFuture verifyIdTokenAsync(@NonNull String idToken) { private CallableOperation verifyIdTokenOp( final String idToken, final boolean checkRevoked) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(idToken), "ID token must not be null or empty"); final FirebaseTokenVerifier verifier = getIdTokenVerifier(checkRevoked); return new CallableOperation() { @@ -380,7 +376,6 @@ public ApiFuture verifySessionCookieAsync(String cookie, boolean private CallableOperation verifySessionCookieOp( final String cookie, final boolean checkRevoked) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty"); final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked); return new CallableOperation() { @@ -434,7 +429,6 @@ public ApiFuture revokeRefreshTokensAsync(@NonNull String uid) { } private CallableOperation revokeRefreshTokensOp(final String uid) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -475,7 +469,6 @@ public ApiFuture getUserAsync(@NonNull String uid) { } private CallableOperation getUserOp(final String uid) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -513,7 +506,6 @@ public ApiFuture getUserByEmailAsync(@NonNull String email) { private CallableOperation getUserByEmailOp( final String email) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -551,7 +543,6 @@ public ApiFuture getUserByPhoneNumberAsync(@NonNull String phoneNumb private CallableOperation getUserByPhoneNumberOp( final String phoneNumber) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number must not be null or empty"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -620,7 +611,6 @@ public ApiFuture listUsersAsync(@Nullable String pageToken, int m private CallableOperation listUsersOp( @Nullable final String pageToken, final int maxResults) { - checkNotDestroyed(); final FirebaseUserManager userManager = getUserManager(); final DefaultUserSource source = new DefaultUserSource(userManager, jsonFactory); final ListUsersPage.Factory factory = new ListUsersPage.Factory(source, maxResults, pageToken); @@ -661,7 +651,6 @@ public ApiFuture createUserAsync(@NonNull UserRecord.CreateRequest r private CallableOperation createUserOp( final UserRecord.CreateRequest request) { - checkNotDestroyed(); checkNotNull(request, "create request must not be null"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -701,7 +690,6 @@ public ApiFuture updateUserAsync(@NonNull UserRecord.UpdateRequest r private CallableOperation updateUserOp( final UserRecord.UpdateRequest request) { - checkNotDestroyed(); checkNotNull(request, "update request must not be null"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -754,7 +742,6 @@ public ApiFuture setCustomUserClaimsAsync( private CallableOperation setCustomUserClaimsOp( final String uid, final Map claims) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -793,7 +780,6 @@ public ApiFuture deleteUserAsync(String uid) { } private CallableOperation deleteUserOp(final String uid) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -876,7 +862,6 @@ public ApiFuture importUsersAsync( private CallableOperation importUsersOp( final List users, final UserImportOptions options) { - checkNotDestroyed(); final UserImportRequest request = new UserImportRequest(users, options, jsonFactory); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -931,7 +916,6 @@ public ApiFuture getUsersAsync(@NonNull Collection getUsersOp( @NonNull final Collection identifiers) { - checkNotDestroyed(); checkNotNull(identifiers, "identifiers must not be null"); checkArgument(identifiers.size() <= FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE, "identifiers parameter must have <= " + FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE @@ -1004,7 +988,6 @@ public ApiFuture deleteUsersAsync(List uids) { private CallableOperation deleteUsersOp( final List uids) { - checkNotDestroyed(); checkNotNull(uids, "uids must not be null"); for (String uid : uids) { UserRecord.checkUid(uid); @@ -1178,7 +1161,6 @@ public ApiFuture generateSignInWithEmailLinkAsync( 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"); @@ -1227,7 +1209,6 @@ public ApiFuture createOidcProviderConfigAsync( private CallableOperation createOidcProviderConfigOp(final OidcProviderConfig.CreateRequest request) { - checkNotDestroyed(); checkNotNull(request, "Create request must not be null."); OidcProviderConfig.checkOidcProviderId(request.getProviderId()); final FirebaseUserManager userManager = getUserManager(); @@ -1271,7 +1252,6 @@ public ApiFuture updateOidcProviderConfigAsync( private CallableOperation updateOidcProviderConfigOp( final OidcProviderConfig.UpdateRequest request) { - checkNotDestroyed(); checkNotNull(request, "Update request must not be null."); checkArgument(!request.getProperties().isEmpty(), "Update request must have at least one property set."); @@ -1316,7 +1296,6 @@ public ApiFuture getOidcProviderConfigAsync(@NonNull String private CallableOperation getOidcProviderConfigOp(final String providerId) { - checkNotDestroyed(); OidcProviderConfig.checkOidcProviderId(providerId); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -1400,7 +1379,6 @@ public ApiFuture> listOidcProviderCo private CallableOperation, FirebaseAuthException> listOidcProviderConfigsOp(@Nullable final String pageToken, final int maxResults) { - checkNotDestroyed(); final FirebaseUserManager userManager = getUserManager(); final DefaultOidcProviderConfigSource source = new DefaultOidcProviderConfigSource(userManager); final ListProviderConfigsPage.Factory factory = @@ -1443,7 +1421,6 @@ public ApiFuture deleteOidcProviderConfigAsync(String providerId) { private CallableOperation deleteOidcProviderConfigOp( final String providerId) { - checkNotDestroyed(); OidcProviderConfig.checkOidcProviderId(providerId); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -1490,7 +1467,6 @@ public ApiFuture createSamlProviderConfigAsync( private CallableOperation createSamlProviderConfigOp(final SamlProviderConfig.CreateRequest request) { - checkNotDestroyed(); checkNotNull(request, "Create request must not be null."); SamlProviderConfig.checkSamlProviderId(request.getProviderId()); final FirebaseUserManager userManager = getUserManager(); @@ -1534,7 +1510,6 @@ public ApiFuture updateSamlProviderConfigAsync( private CallableOperation updateSamlProviderConfigOp( final SamlProviderConfig.UpdateRequest request) { - checkNotDestroyed(); checkNotNull(request, "Update request must not be null."); checkArgument(!request.getProperties().isEmpty(), "Update request must have at least one property set."); @@ -1579,7 +1554,6 @@ public ApiFuture getSamlProviderConfigAsync(@NonNull String private CallableOperation getSamlProviderConfigOp(final String providerId) { - checkNotDestroyed(); SamlProviderConfig.checkSamlProviderId(providerId); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -1663,7 +1637,6 @@ public ApiFuture> listSamlProviderCo private CallableOperation, FirebaseAuthException> listSamlProviderConfigsOp(@Nullable final String pageToken, final int maxResults) { - checkNotDestroyed(); final FirebaseUserManager userManager = getUserManager(); final DefaultSamlProviderConfigSource source = new DefaultSamlProviderConfigSource(userManager); final ListProviderConfigsPage.Factory factory = @@ -1706,7 +1679,6 @@ public ApiFuture deleteSamlProviderConfigAsync(String providerId) { private CallableOperation deleteSamlProviderConfigOp( final String providerId) { - checkNotDestroyed(); SamlProviderConfig.checkSamlProviderId(providerId); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -1729,32 +1701,12 @@ Supplier threadSafeMemoize(final Supplier supplier) { public T get() { checkNotNull(supplier); synchronized (lock) { - checkNotDestroyed(); return supplier.get(); } } }); } - private void checkNotDestroyed() { - synchronized (lock) { - checkState( - !destroyed.get(), - "FirebaseAuth instance is no longer alive. This happens when " - + "the parent FirebaseApp instance has been deleted."); - } - } - - final void destroy() { - synchronized (lock) { - doDestroy(); - destroyed.set(true); - } - } - - /** Performs any additional required clean up. */ - protected abstract void doDestroy(); - protected abstract static class Builder> { private FirebaseApp firebaseApp; diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 417ea6436..27e79960d 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -69,9 +69,6 @@ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { return service.getInstance(); } - @Override - protected void doDestroy() { } - private static FirebaseAuth fromApp(final FirebaseApp app) { return populateBuilderFromApp(builder(), app, null) .setTenantManager(new Supplier() { @@ -88,11 +85,6 @@ private static class FirebaseAuthService extends FirebaseService { FirebaseAuthService(FirebaseApp app) { super(SERVICE_ID, FirebaseAuth.fromApp(app)); } - - @Override - public void destroy() { - instance.destroy(); - } } static Builder builder() { diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java index 95ef169da..bc374c036 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java @@ -69,11 +69,6 @@ public ApiFuture apply(FirebaseToken input) { }, MoreExecutors.directExecutor()); } - @Override - protected void doDestroy() { - // Nothing extra needs to be destroyed. - } - static TenantAwareFirebaseAuth fromApp(FirebaseApp app, String tenantId) { return populateBuilderFromApp(builder(), app, tenantId) .setTenantId(tenantId) diff --git a/src/main/java/com/google/firebase/cloud/StorageClient.java b/src/main/java/com/google/firebase/cloud/StorageClient.java index d6a1567f4..955abd7e2 100644 --- a/src/main/java/com/google/firebase/cloud/StorageClient.java +++ b/src/main/java/com/google/firebase/cloud/StorageClient.java @@ -106,13 +106,5 @@ private static class StorageClientService extends FirebaseService StorageClientService(StorageClient client) { super(SERVICE_ID, client); } - - @Override - public void destroy() { - // NOTE: We don't explicitly tear down anything here, but public methods of StorageClient - // will now fail because calls to getOptions() and getToken() will hit FirebaseApp, - // which will throw once the app is deleted. - } } - } diff --git a/src/main/java/com/google/firebase/database/FirebaseDatabase.java b/src/main/java/com/google/firebase/database/FirebaseDatabase.java index 0a79932db..306ce9274 100644 --- a/src/main/java/com/google/firebase/database/FirebaseDatabase.java +++ b/src/main/java/com/google/firebase/database/FirebaseDatabase.java @@ -173,7 +173,7 @@ static FirebaseDatabase createForTests( return db; } - /** + /** * @return The version for this build of the Firebase Database client */ public static String getSdkVersion() { @@ -358,6 +358,10 @@ DatabaseConfig getConfig() { return this.config; } + /** + * Tears down the WebSocket connections and background threads started by this {@code + * FirebaseDatabase} instance thus disconnecting from the remote database. + */ void destroy() { synchronized (lock) { if (destroyed.get()) { diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java index 822654402..47026e6f1 100644 --- a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java +++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java @@ -200,12 +200,5 @@ private static class FirebaseInstanceIdService extends FirebaseService Type of the service */ -public abstract class FirebaseService { +public class FirebaseService { private final String id; protected final T instance; @@ -62,5 +62,7 @@ public final T getInstance() { * Tear down this FirebaseService instance and the service object wrapped in it, cleaning up * any allocated resources in the process. */ - public abstract void destroy(); + public void destroy() { + // Child classes can override this method to implement any service-specific cleanup logic. + } } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index e1b4f794c..a08e8cfaf 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -403,13 +403,6 @@ private static class FirebaseMessagingService extends FirebaseService { + MockFirebaseService() { + super("MockFirebaseService", new Object()); + } + } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index e34fede1f..bd7aa0ff8 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -28,7 +28,6 @@ import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.api.core.ApiFuture; -import com.google.common.base.Defaults; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMap; @@ -40,11 +39,6 @@ import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestResponseInterceptor; import com.google.firebase.testing.TestUtils; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -105,26 +99,33 @@ public void testInvokeAfterAppDelete() throws Exception { assertNotNull(auth); app.delete(); - for (Method method : auth.getClass().getDeclaredMethods()) { - int modifiers = method.getModifiers(); - if (!Modifier.isPublic(modifiers) || Modifier.isStatic(modifiers)) { - continue; - } - - List parameters = new ArrayList<>(method.getParameterTypes().length); - for (Class parameterType : method.getParameterTypes()) { - parameters.add(Defaults.defaultValue(parameterType)); - } - try { - method.invoke(auth, parameters.toArray()); - fail("No error thrown when invoking auth after deleting app; method: " + method.getName()); - } catch (InvocationTargetException expected) { - String message = "FirebaseAuth instance is no longer alive. This happens when " - + "the parent FirebaseApp instance has been deleted."; - Throwable cause = expected.getCause(); - assertTrue(cause instanceof IllegalStateException); - assertEquals(message, cause.getMessage()); - } + String message = "FirebaseApp 'testInvokeAfterAppDelete' was deleted"; + try { + FirebaseAuth.getInstance(app); + fail("No error thrown when invoking auth after deleting app"); + } catch (IllegalStateException ex) { + assertEquals(message, ex.getMessage()); + } + + try { + auth.createCustomToken("uid"); + fail("No error thrown when invoking auth after deleting app"); + } catch (IllegalStateException ex) { + assertEquals(message, ex.getMessage()); + } + + try { + auth.verifyIdToken("idToken"); + fail("No error thrown when invoking auth after deleting app"); + } catch (IllegalStateException ex) { + assertEquals(message, ex.getMessage()); + } + + try { + auth.getUser("uid"); + fail("No error thrown when invoking auth after deleting app"); + } catch (IllegalStateException ex) { + assertEquals(message, ex.getMessage()); } } @@ -461,10 +462,6 @@ public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws Interru } } - private FirebaseToken getFirebaseToken(String subject) { - return new FirebaseToken(ImmutableMap.of("sub", subject)); - } - private FirebaseToken getFirebaseToken(long issuedAt) { return new FirebaseToken(ImmutableMap.of("sub", TEST_USER, "iat", issuedAt)); } diff --git a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java index f15861a62..27864d992 100644 --- a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java +++ b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java @@ -50,6 +50,11 @@ public class FirebaseInstanceIdTest { + private static final FirebaseOptions APP_OPTIONS = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + private static final Map ERROR_MESSAGES = ImmutableMap.of( 404, "Instance ID \"test-iid\": Failed to find the instance ID.", 409, "Instance ID \"test-iid\": Already deleted.", @@ -88,13 +93,25 @@ public void testNoProjectId() { } } + @Test + public void testInvokeAfterAppDelete() { + FirebaseApp app = FirebaseApp.initializeApp(APP_OPTIONS, "testInvokeAfterAppDelete"); + FirebaseInstanceId instanceId = FirebaseInstanceId.getInstance(app); + assertNotNull(instanceId); + app.delete(); + + try { + FirebaseInstanceId.getInstance(app); + fail("No error thrown when invoking instanceId after deleting app"); + } catch (IllegalStateException ex) { + String message = "FirebaseApp 'testInvokeAfterAppDelete' was deleted"; + assertEquals(message, ex.getMessage()); + } + } + @Test public void testInvalidInstanceId() { - FirebaseOptions options = FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - FirebaseApp.initializeApp(options); + FirebaseApp.initializeApp(APP_OPTIONS); FirebaseInstanceId instanceId = FirebaseInstanceId.getInstance(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); @@ -122,9 +139,7 @@ public void testDeleteInstanceId() throws Exception { MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(response) .build(); - FirebaseOptions options = FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") + FirebaseOptions options = APP_OPTIONS.toBuilder() .setHttpTransport(transport) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -168,9 +183,7 @@ public void testDeleteInstanceIdError() throws Exception { MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(response) .build(); - FirebaseOptions options = FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") + FirebaseOptions options = APP_OPTIONS.toBuilder() .setHttpTransport(transport) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); From fba507bd4cff639865653da717e7b1a5dafea8fd Mon Sep 17 00:00:00 2001 From: kentengjin Date: Wed, 30 Sep 2020 11:41:22 -0700 Subject: [PATCH 134/456] fix(auth): Migrate IAM SignBlob to IAMCredentials SignBlob (#480) * Migrate IAM SignBlob to IAMCredentials SignBlob Point all SignBlob calls to IAMCredentials * Fix FirebaseToken tests Change proto field to fix the test --- .../google/firebase/auth/internal/CryptoSigners.java | 10 +++++----- .../firebase/auth/FirebaseCustomTokenTest.java | 4 ++-- .../firebase/auth/internal/CryptoSignersTest.java | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java index 7bc54afdf..f167b09a0 100644 --- a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java +++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java @@ -65,13 +65,13 @@ public String getAccount() { /** * @ {@link CryptoSigner} implementation that uses the - * - * Google IAM service to sign data. + * + * Google IAMCredentials 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"; + "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob"; private final String serviceAccount; private final ErrorHandlingHttpClient httpClient; @@ -95,11 +95,11 @@ void setInterceptor(HttpResponseInterceptor interceptor) { @Override public byte[] sign(byte[] payload) throws FirebaseAuthException { String encodedPayload = BaseEncoding.base64().encode(payload); - Map content = ImmutableMap.of("bytesToSign", encodedPayload); + Map content = ImmutableMap.of("payload", encodedPayload); String encodedUrl = String.format(IAM_SIGN_BLOB_URL, serviceAccount); HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPostRequest(encodedUrl, content); GenericJson parsed = httpClient.sendAndParse(requestInfo, GenericJson.class); - return BaseEncoding.base64().decode((String) parsed.get("signature")); + return BaseEncoding.base64().decode((String) parsed.get("signedBlob")); } @Override diff --git a/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java b/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java index b277dce14..5e37161cd 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java @@ -92,7 +92,7 @@ public void testCreateCustomTokenWithDeveloperClaims() throws Exception { public void testCreateCustomTokenWithoutServiceAccountCredentials() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); String content = Utils.getDefaultJsonFactory().toString( - ImmutableMap.of("signature", BaseEncoding.base64().encode("test-signature".getBytes()))); + ImmutableMap.of("signedBlob", BaseEncoding.base64().encode("test-signature".getBytes()))); response.setContent(content); MockHttpTransport transport = new MultiRequestMockHttpTransport(ImmutableList.of(response)); @@ -117,7 +117,7 @@ public void testCreateCustomTokenWithoutServiceAccountCredentials() throws Excep @Test public void testCreateCustomTokenWithDiscoveredServiceAccount() throws Exception { String content = Utils.getDefaultJsonFactory().toString( - ImmutableMap.of("signature", BaseEncoding.base64().encode("test-signature".getBytes()))); + ImmutableMap.of("signedBlob", BaseEncoding.base64().encode("test-signature".getBytes()))); List responses = ImmutableList.of( // Service account discovery response new MockLowLevelHttpResponse().setContent("test@service.account"), diff --git a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java index 32f9fd543..fee4d8fb8 100644 --- a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java +++ b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java @@ -71,7 +71,7 @@ public void testInvalidServiceAccountCryptoSigner() { public void testIAMCryptoSigner() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( - ImmutableMap.of("signature", signature)); + ImmutableMap.of("signedBlob", signature)); MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent(response)) .build(); @@ -84,7 +84,7 @@ public void testIAMCryptoSigner() throws Exception { byte[] data = signer.sign("foo".getBytes()); assertArrayEquals("signed-bytes".getBytes(), data); - final String url = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + final String url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" + "test-service-account@iam.gserviceaccount.com:signBlob"; assertEquals(url, interceptor.getResponse().getRequest().getUrl().toString()); } @@ -150,7 +150,7 @@ public void testInvalidIAMCryptoSigner() { public void testMetadataService() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( - ImmutableMap.of("signature", signature)); + ImmutableMap.of("signedBlob", signature)); MockHttpTransport transport = new MultiRequestMockHttpTransport( ImmutableList.of( new MockLowLevelHttpResponse().setContent("metadata-server@iam.gserviceaccount.com"), @@ -168,7 +168,7 @@ public void testMetadataService() throws Exception { byte[] data = signer.sign("foo".getBytes()); assertArrayEquals("signed-bytes".getBytes(), data); - final String url = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + final String url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" + "metadata-server@iam.gserviceaccount.com:signBlob"; HttpRequest request = interceptor.getResponse().getRequest(); assertEquals(url, request.getUrl().toString()); @@ -179,7 +179,7 @@ public void testMetadataService() throws Exception { public void testExplicitServiceAccountEmail() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( - ImmutableMap.of("signature", signature)); + ImmutableMap.of("signedBlob", signature)); // Explicit service account should get precedence MockHttpTransport transport = new MultiRequestMockHttpTransport( @@ -198,7 +198,7 @@ public void testExplicitServiceAccountEmail() throws Exception { byte[] data = signer.sign("foo".getBytes()); assertArrayEquals("signed-bytes".getBytes(), data); - final String url = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + final String url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" + "explicit-service-account@iam.gserviceaccount.com:signBlob"; HttpRequest request = interceptor.getResponse().getRequest(); assertEquals(url, request.getUrl().toString()); From 408b85f7d0072e8fb3b462aa2317843d683e7ad7 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 7 Oct 2020 15:50:03 -0400 Subject: [PATCH 135/456] [chore] Release 7.0.1 (#484) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0f40ea078..0bde2d2f4 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 7.0.0 + 7.0.1 jar firebase-admin From ba56dcde18fb6cade53269edcca75f5ac101df4b Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Thu, 15 Oct 2020 14:35:23 -0400 Subject: [PATCH 136/456] Fix comment about max number of events (#488) Multicast message can send up to 500 messages, not 100 https://github.com/firebase/firebase-admin-java/blob/408b85f7d0072e8fb3b462aa2317843d683e7ad7/src/main/java/com/google/firebase/messaging/MulticastMessage.java#L46 --- .../com/google/firebase/snippets/FirebaseMessagingSnippets.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java index f1432e160..4a4a25472 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java @@ -151,7 +151,7 @@ public void sendAll() throws FirebaseMessagingException { public void sendMulticast() throws FirebaseMessagingException { // [START send_multicast] - // Create a list containing up to 100 registration tokens. + // Create a list containing up to 500 registration tokens. // These registration tokens come from the client FCM SDKs. List registrationTokens = Arrays.asList( "YOUR_REGISTRATION_TOKEN_1", From 5f3b696db4bf4550203a47834dfa19a8a33d750b Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Mon, 14 Dec 2020 14:40:18 -0500 Subject: [PATCH 137/456] feat(rc): Add Remote Config Management API (#502) - Add Remote Config Management API --- .../firebase/remoteconfig/Condition.java | 174 +++ .../remoteconfig/FirebaseRemoteConfig.java | 415 ++++++ .../FirebaseRemoteConfigClient.java | 43 + .../FirebaseRemoteConfigClientImpl.java | 269 ++++ .../FirebaseRemoteConfigException.java | 63 + .../remoteconfig/ListVersionsOptions.java | 198 +++ .../remoteconfig/ListVersionsPage.java | 268 ++++ .../firebase/remoteconfig/Parameter.java | 167 +++ .../firebase/remoteconfig/ParameterGroup.java | 138 ++ .../firebase/remoteconfig/ParameterValue.java | 127 ++ .../firebase/remoteconfig/PublishOptions.java | 33 + .../remoteconfig/RemoteConfigErrorCode.java | 60 + .../remoteconfig/RemoteConfigUtil.java | 93 ++ .../firebase/remoteconfig/TagColor.java | 45 + .../firebase/remoteconfig/Template.java | 281 ++++ .../google/firebase/remoteconfig/User.java | 98 ++ .../google/firebase/remoteconfig/Version.java | 225 +++ .../RemoteConfigServiceErrorResponse.java | 72 + .../internal/TemplateResponse.java | 424 ++++++ .../remoteconfig/internal/package-info.java | 20 + .../firebase/remoteconfig/ConditionTest.java | 93 ++ .../FirebaseRemoteConfigClientImplTest.java | 1234 +++++++++++++++++ .../remoteconfig/FirebaseRemoteConfigIT.java | 199 +++ .../FirebaseRemoteConfigTest.java | 600 ++++++++ .../remoteconfig/ListVersionsPageTest.java | 338 +++++ .../remoteconfig/MockRemoteConfigClient.java | 89 ++ .../remoteconfig/ParameterGroupTest.java | 87 ++ .../firebase/remoteconfig/ParameterTest.java | 91 ++ .../remoteconfig/ParameterValueTest.java | 54 + .../firebase/remoteconfig/TemplateTest.java | 346 +++++ .../firebase/remoteconfig/UserTest.java | 62 + .../firebase/remoteconfig/VersionTest.java | 105 ++ src/test/resources/getRemoteConfig.json | 56 + .../resources/listRemoteConfigVersions.json | 50 + 34 files changed, 6617 insertions(+) create mode 100644 src/main/java/com/google/firebase/remoteconfig/Condition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ListVersionsOptions.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ListVersionsPage.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/Parameter.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ParameterGroup.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ParameterValue.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/PublishOptions.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/RemoteConfigErrorCode.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/TagColor.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/Template.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/User.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/Version.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/internal/RemoteConfigServiceErrorResponse.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/internal/package-info.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ConditionTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigIT.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ListVersionsPageTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ParameterGroupTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ParameterTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/TemplateTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/UserTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/VersionTest.java create mode 100644 src/test/resources/getRemoteConfig.json create mode 100644 src/test/resources/listRemoteConfigVersions.json diff --git a/src/main/java/com/google/firebase/remoteconfig/Condition.java b/src/main/java/com/google/firebase/remoteconfig/Condition.java new file mode 100644 index 000000000..6ccde59d9 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/Condition.java @@ -0,0 +1,174 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.remoteconfig; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Strings; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ConditionResponse; + +import java.util.Objects; + +/** + * Represents a Remote Config condition that can be included in a {@link Template}. + * A condition targets a specific group of users. A list of these conditions make up + * part of a Remote Config template. + */ +public final class Condition { + + private String name; + private String expression; + private TagColor tagColor; + + /** + * Creates a new {@link Condition}. + * + * @param name A non-null, non-empty, and unique name of this condition. + * @param expression A non-null and non-empty expression of this condition. + */ + public Condition(@NonNull String name, @NonNull String expression) { + this(name, expression, null); + } + + /** + * Creates a new {@link Condition}. + * + * @param name A non-null, non-empty, and unique name of this condition. + * @param expression A non-null and non-empty expression of this condition. + * @param tagColor A color associated with this condition for display purposes in the + * Firebase Console. Not specifying this value results in the console picking an + * arbitrary color to associate with the condition. + */ + public Condition(@NonNull String name, @NonNull String expression, @Nullable TagColor tagColor) { + checkArgument(!Strings.isNullOrEmpty(name), "condition name must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(expression), + "condition expression must not be null or empty"); + this.name = name; + this.expression = expression; + this.tagColor = tagColor; + } + + Condition(@NonNull ConditionResponse conditionResponse) { + checkNotNull(conditionResponse); + this.name = conditionResponse.getName(); + this.expression = conditionResponse.getExpression(); + if (!Strings.isNullOrEmpty(conditionResponse.getTagColor())) { + this.tagColor = TagColor.valueOf(conditionResponse.getTagColor()); + } + } + + /** + * Gets the name of the condition. + * + * @return The name of the condition. + */ + @NonNull + public String getName() { + return name; + } + + /** + * Gets the expression of the condition. + * + * @return The expression of the condition. + */ + @NonNull + public String getExpression() { + return expression; + } + + /** + * Gets the tag color of the condition used for display purposes in the Firebase Console. + * + * @return The tag color of the condition. + */ + @NonNull + public TagColor getTagColor() { + return tagColor; + } + + /** + * Sets the name of the condition. + * + * @param name A non-empty and unique name of this condition. + * @return This {@link Condition}. + */ + public Condition setName(@NonNull String name) { + checkArgument(!Strings.isNullOrEmpty(name), "condition name must not be null or empty"); + this.name = name; + return this; + } + + /** + * Sets the expression of the condition. + * + *

      See + * condition expressions for the expected syntax of this field. + * + * @param expression The logic of this condition. + * @return This {@link Condition}. + */ + public Condition setExpression(@NonNull String expression) { + checkArgument(!Strings.isNullOrEmpty(expression), + "condition expression must not be null or empty"); + this.expression = expression; + return this; + } + + /** + * Sets the tag color of the condition. + * + *

      The color associated with this condition for display purposes in the Firebase Console. + * Not specifying this value results in the console picking an arbitrary color to associate + * with the condition. + * + * @param tagColor The tag color of this condition. + * @return This {@link Condition}. + */ + public Condition setTagColor(@Nullable TagColor tagColor) { + this.tagColor = tagColor; + return this; + } + + ConditionResponse toConditionResponse() { + return new ConditionResponse() + .setName(this.name) + .setExpression(this.expression) + .setTagColor(this.tagColor == null ? null : this.tagColor.getColor()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Condition condition = (Condition) o; + return Objects.equals(name, condition.name) + && Objects.equals(expression, condition.expression) && tagColor == condition.tagColor; + } + + @Override + public int hashCode() { + return Objects.hash(name, expression, tagColor); + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java new file mode 100644 index 000000000..41a0afbe4 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java @@ -0,0 +1,415 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.remoteconfig; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.core.ApiFuture; +import com.google.common.annotations.VisibleForTesting; +import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.internal.CallableOperation; +import com.google.firebase.internal.FirebaseService; +import com.google.firebase.internal.NonNull; + +/** + * This class is the entry point for all server-side Firebase Remote Config actions. + * + *

      You can get an instance of {@link FirebaseRemoteConfig} via {@link #getInstance(FirebaseApp)}, + * and then use it to manage Remote Config templates. + */ +public final class FirebaseRemoteConfig { + + private static final String SERVICE_ID = FirebaseRemoteConfig.class.getName(); + private final FirebaseApp app; + private final FirebaseRemoteConfigClient remoteConfigClient; + + @VisibleForTesting + FirebaseRemoteConfig(FirebaseApp app, FirebaseRemoteConfigClient client) { + this.app = checkNotNull(app); + this.remoteConfigClient = checkNotNull(client); + } + + private FirebaseRemoteConfig(FirebaseApp app) { + this(app, FirebaseRemoteConfigClientImpl.fromApp(app)); + } + + /** + * Gets the {@link FirebaseRemoteConfig} instance for the default {@link FirebaseApp}. + * + * @return The {@link FirebaseRemoteConfig} instance for the default {@link FirebaseApp}. + */ + public static FirebaseRemoteConfig getInstance() { + return getInstance(FirebaseApp.getInstance()); + } + + /** + * Gets the {@link FirebaseRemoteConfig} instance for the specified {@link FirebaseApp}. + * + * @return The {@link FirebaseRemoteConfig} instance for the specified {@link FirebaseApp}. + */ + public static synchronized FirebaseRemoteConfig getInstance(FirebaseApp app) { + FirebaseRemoteConfigService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID, + FirebaseRemoteConfigService.class); + if (service == null) { + service = ImplFirebaseTrampolines.addService(app, new FirebaseRemoteConfigService(app)); + } + return service.getInstance(); + } + + /** + * Gets the current active version of the Remote Config template. + * + * @return A {@link Template}. + * @throws FirebaseRemoteConfigException If an error occurs while getting the template. + */ + public Template getTemplate() throws FirebaseRemoteConfigException { + return getTemplateOp().call(); + } + + /** + * Similar to {@link #getTemplate()} but performs the operation asynchronously. + * + * @return An {@code ApiFuture} that completes with a {@link Template} when + * the template is available. + */ + public ApiFuture