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() {