attach-javadocs
diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
index b9b563e69..f779ae9df 100644
--- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
@@ -38,8 +38,11 @@
import com.google.firebase.internal.NonNull;
import com.google.firebase.internal.Nullable;
import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -762,6 +765,140 @@ protected UserImportResult execute() throws FirebaseAuthException {
};
}
+ /**
+ * Gets the user data corresponding to the specified identifiers.
+ *
+ * There are no ordering guarantees; in particular, the nth entry in the users result list is
+ * not guaranteed to correspond to the nth entry in the input parameters list.
+ *
+ *
A maximum of 100 identifiers may be specified. If more than 100 identifiers are
+ * supplied, this method throws an {@link IllegalArgumentException}.
+ *
+ * @param identifiers The identifiers used to indicate which user records should be returned. Must
+ * have 100 or fewer entries.
+ * @return The corresponding user records.
+ * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100
+ * identifiers are specified.
+ * @throws NullPointerException If the identifiers parameter is null.
+ * @throws FirebaseAuthException If an error occurs while retrieving user data.
+ */
+ public GetUsersResult getUsers(@NonNull Collection identifiers)
+ throws FirebaseAuthException {
+ return getUsersOp(identifiers).call();
+ }
+
+ /**
+ * Gets the user data corresponding to the specified identifiers.
+ *
+ * There are no ordering guarantees; in particular, the nth entry in the users result list is
+ * not guaranteed to correspond to the nth entry in the input parameters list.
+ *
+ *
A maximum of 100 identifiers may be specified. If more than 100 identifiers are
+ * supplied, this method throws an {@link IllegalArgumentException}.
+ *
+ * @param identifiers The identifiers used to indicate which user records should be returned.
+ * Must have 100 or fewer entries.
+ * @return An {@code ApiFuture} that resolves to the corresponding user records.
+ * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100
+ * identifiers are specified.
+ * @throws NullPointerException If the identifiers parameter is null.
+ */
+ public ApiFuture getUsersAsync(@NonNull Collection identifiers) {
+ return getUsersOp(identifiers).callAsync(firebaseApp);
+ }
+
+ private CallableOperation getUsersOp(
+ @NonNull final Collection identifiers) {
+ checkNotDestroyed();
+ checkNotNull(identifiers, "identifiers must not be null");
+ checkArgument(identifiers.size() <= FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE,
+ "identifiers parameter must have <= " + FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE
+ + " entries.");
+
+ final FirebaseUserManager userManager = getUserManager();
+ return new CallableOperation() {
+ @Override
+ protected GetUsersResult execute() throws FirebaseAuthException {
+ Set users = userManager.getAccountInfo(identifiers);
+ Set notFound = new HashSet<>();
+ for (UserIdentifier id : identifiers) {
+ if (!isUserFound(id, users)) {
+ notFound.add(id);
+ }
+ }
+ return new GetUsersResult(users, notFound);
+ }
+ };
+ }
+
+ private boolean isUserFound(UserIdentifier id, Collection userRecords) {
+ for (UserRecord userRecord : userRecords) {
+ if (id.matches(userRecord)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Deletes the users specified by the given identifiers.
+ *
+ * Deleting a non-existing user does not generate an error (the method is idempotent).
+ * Non-existing users are considered to be successfully deleted and are therefore included in the
+ * DeleteUsersResult.getSuccessCount() value.
+ *
+ *
A maximum of 1000 identifiers may be supplied. If more than 1000 identifiers are
+ * supplied, this method throws an {@link IllegalArgumentException}.
+ *
+ *
This API has a rate limit of 1 QPS. Exceeding the limit may result in a quota exceeded
+ * error. If you want to delete more than 1000 users, we suggest adding a delay to ensure you
+ * don't exceed this limit.
+ *
+ * @param uids The uids of the users to be deleted. Must have <= 1000 entries.
+ * @return The total number of successful/failed deletions, as well as the array of errors that
+ * correspond to the failed deletions.
+ * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000
+ * identifiers are specified.
+ * @throws FirebaseAuthException If an error occurs while deleting users.
+ */
+ public DeleteUsersResult deleteUsers(List uids) throws FirebaseAuthException {
+ return deleteUsersOp(uids).call();
+ }
+
+ /**
+ * Similar to {@link #deleteUsers(List)} but performs the operation asynchronously.
+ *
+ * @param uids The uids of the users to be deleted. Must have <= 1000 entries.
+ * @return An {@code ApiFuture} that resolves to the total number of successful/failed
+ * deletions, as well as the array of errors that correspond to the failed deletions. If an
+ * error occurs while deleting the user account, the future throws a
+ * {@link FirebaseAuthException}.
+ * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000
+ * identifiers are specified.
+ */
+ public ApiFuture deleteUsersAsync(List uids) {
+ return deleteUsersOp(uids).callAsync(firebaseApp);
+ }
+
+ private CallableOperation deleteUsersOp(
+ final List uids) {
+ checkNotDestroyed();
+ checkNotNull(uids, "uids must not be null");
+ for (String uid : uids) {
+ UserRecord.checkUid(uid);
+ }
+ checkArgument(uids.size() <= FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE,
+ "uids parameter must have <= " + FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE
+ + " entries.");
+ final FirebaseUserManager userManager = getUserManager();
+ return new CallableOperation() {
+ @Override
+ protected DeleteUsersResult execute() throws FirebaseAuthException {
+ return userManager.deleteUsers(uids);
+ }
+ };
+ }
+
/**
* Generates the out-of-band email action link for password reset flows for the specified email
* address.
diff --git a/src/main/java/com/google/firebase/auth/DeleteUsersResult.java b/src/main/java/com/google/firebase/auth/DeleteUsersResult.java
new file mode 100644
index 000000000..e8ca7dba5
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/DeleteUsersResult.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.auth.internal.BatchDeleteResponse;
+import com.google.firebase.internal.NonNull;
+import java.util.List;
+
+/**
+ * Represents the result of the {@link FirebaseAuth#deleteUsersAsync(List)} API.
+ */
+public final class DeleteUsersResult {
+
+ private final int successCount;
+ private final List errors;
+
+ DeleteUsersResult(int users, BatchDeleteResponse response) {
+ ImmutableList.Builder errorsBuilder = ImmutableList.builder();
+ List responseErrors = response.getErrors();
+ if (responseErrors != null) {
+ checkArgument(users >= responseErrors.size());
+ for (BatchDeleteResponse.ErrorInfo error : responseErrors) {
+ errorsBuilder.add(new ErrorInfo(error.getIndex(), error.getMessage()));
+ }
+ }
+ errors = errorsBuilder.build();
+ successCount = users - errors.size();
+ }
+
+ /**
+ * Returns the number of users that were deleted successfully (possibly zero). Users that did not
+ * exist prior to calling {@link FirebaseAuth#deleteUsersAsync(List)} are considered to be
+ * successfully deleted.
+ */
+ public int getSuccessCount() {
+ return successCount;
+ }
+
+ /**
+ * Returns the number of users that failed to be deleted (possibly zero).
+ */
+ public int getFailureCount() {
+ return errors.size();
+ }
+
+ /**
+ * A list of {@link ErrorInfo} instances describing the errors that were encountered during
+ * the deletion. Length of this list is equal to the return value of
+ * {@link #getFailureCount()}.
+ *
+ * @return A non-null list (possibly empty).
+ */
+ @NonNull
+ public List getErrors() {
+ return errors;
+ }
+}
diff --git a/src/main/java/com/google/firebase/auth/EmailIdentifier.java b/src/main/java/com/google/firebase/auth/EmailIdentifier.java
new file mode 100644
index 000000000..8e729c220
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/EmailIdentifier.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import com.google.firebase.auth.internal.GetAccountInfoRequest;
+import com.google.firebase.internal.NonNull;
+
+/**
+ * Used for looking up an account by email.
+ *
+ * @see {FirebaseAuth#getUsers}
+ */
+public final class EmailIdentifier extends UserIdentifier {
+ private final String email;
+
+ public EmailIdentifier(@NonNull String email) {
+ UserRecord.checkEmail(email);
+ this.email = email;
+ }
+
+ @Override
+ public String toString() {
+ return "EmailIdentifier(" + email + ")";
+ }
+
+ @Override
+ void populate(@NonNull GetAccountInfoRequest payload) {
+ payload.addEmail(email);
+ }
+
+ @Override
+ boolean matches(@NonNull UserRecord userRecord) {
+ return email.equals(userRecord.getEmail());
+ }
+}
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
index ce8c30a11..ff52acad7 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
@@ -26,7 +26,6 @@
import com.google.common.base.Supplier;
import com.google.firebase.FirebaseApp;
import com.google.firebase.ImplFirebaseTrampolines;
-import com.google.firebase.auth.AbstractFirebaseAuth.Builder;
import com.google.firebase.auth.internal.FirebaseTokenFactory;
import com.google.firebase.internal.CallableOperation;
import com.google.firebase.internal.FirebaseService;
@@ -41,7 +40,7 @@
* custom tokens for use by client-side code, verifying Firebase ID Tokens received from clients, or
* creating new FirebaseApp instances that are scoped to a particular authentication UID.
*/
-public class FirebaseAuth extends AbstractFirebaseAuth {
+public final class FirebaseAuth extends AbstractFirebaseAuth {
private static final String SERVICE_ID = FirebaseAuth.class.getName();
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index 4e6ce0eab..e5d39b87f 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -39,8 +39,9 @@
import com.google.common.collect.ImmutableSortedSet;
import com.google.firebase.FirebaseApp;
import com.google.firebase.ImplFirebaseTrampolines;
-import com.google.firebase.auth.UserRecord;
+import com.google.firebase.auth.internal.BatchDeleteResponse;
import com.google.firebase.auth.internal.DownloadAccountResponse;
+import com.google.firebase.auth.internal.GetAccountInfoRequest;
import com.google.firebase.auth.internal.GetAccountInfoResponse;
import com.google.firebase.auth.internal.HttpErrorResponse;
import com.google.firebase.auth.internal.ListTenantsResponse;
@@ -51,8 +52,11 @@
import com.google.firebase.internal.SdkUtils;
import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* FirebaseUserManager provides methods for interacting with the Google Identity Toolkit via its
@@ -94,6 +98,8 @@ class FirebaseUserManager {
.build();
static final int MAX_LIST_TENANTS_RESULTS = 1000;
+ static final int MAX_GET_ACCOUNTS_BATCH_SIZE = 100;
+ static final int MAX_DELETE_ACCOUNTS_BATCH_SIZE = 1000;
static final int MAX_LIST_USERS_RESULTS = 1000;
static final int MAX_IMPORT_USERS = 1000;
@@ -175,6 +181,33 @@ UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException
return new UserRecord(response.getUsers().get(0), jsonFactory);
}
+ Set getAccountInfo(@NonNull Collection identifiers)
+ throws FirebaseAuthException {
+ if (identifiers.isEmpty()) {
+ return new HashSet<>();
+ }
+
+ GetAccountInfoRequest payload = new GetAccountInfoRequest();
+ for (UserIdentifier id : identifiers) {
+ id.populate(payload);
+ }
+
+ GetAccountInfoResponse response = post(
+ "/accounts:lookup", payload, GetAccountInfoResponse.class);
+
+ if (response == null) {
+ throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to parse server response");
+ }
+
+ Set results = new HashSet<>();
+ if (response.getUsers() != null) {
+ for (GetAccountInfoResponse.User user : response.getUsers()) {
+ results.add(new UserRecord(user, jsonFactory));
+ }
+ }
+ return results;
+ }
+
String createUser(UserRecord.CreateRequest request) throws FirebaseAuthException {
GenericJson response = post(
"/accounts", request.getProperties(), GenericJson.class);
@@ -205,6 +238,23 @@ void deleteUser(String uid) throws FirebaseAuthException {
}
}
+ /**
+ * @pre uids != null
+ * @pre uids.size() <= MAX_DELETE_ACCOUNTS_BATCH_SIZE
+ */
+ DeleteUsersResult deleteUsers(@NonNull List uids) throws FirebaseAuthException {
+ final Map payload = ImmutableMap.of(
+ "localIds", uids,
+ "force", true);
+ BatchDeleteResponse response = post(
+ "/accounts:batchDelete", payload, BatchDeleteResponse.class);
+ if (response == null) {
+ throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete users");
+ }
+
+ return new DeleteUsersResult(uids.size(), response);
+ }
+
DownloadAccountResponse listUsers(int maxResults, String pageToken) throws FirebaseAuthException {
ImmutableMap.Builder builder = ImmutableMap.builder()
.put("maxResults", maxResults);
diff --git a/src/main/java/com/google/firebase/auth/GetUsersResult.java b/src/main/java/com/google/firebase/auth/GetUsersResult.java
new file mode 100644
index 000000000..3ceec01cb
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/GetUsersResult.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.firebase.internal.NonNull;
+import java.util.Set;
+
+/**
+ * Represents the result of the {@link FirebaseAuth#getUsersAsync(Collection)} API.
+ */
+public final class GetUsersResult {
+ private final Set users;
+ private final Set notFound;
+
+ GetUsersResult(@NonNull Set users, @NonNull Set notFound) {
+ this.users = checkNotNull(users);
+ this.notFound = checkNotNull(notFound);
+ }
+
+ /**
+ * Set of user records corresponding to the set of users that were requested. Only users
+ * that were found are listed here. The result set is unordered.
+ */
+ @NonNull
+ public Set getUsers() {
+ return this.users;
+ }
+
+ /**
+ * Set of identifiers that were requested, but not found.
+ */
+ @NonNull
+ public Set getNotFound() {
+ return this.notFound;
+ }
+}
diff --git a/src/main/java/com/google/firebase/auth/PhoneIdentifier.java b/src/main/java/com/google/firebase/auth/PhoneIdentifier.java
new file mode 100644
index 000000000..bdc84fe92
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/PhoneIdentifier.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import com.google.firebase.auth.internal.GetAccountInfoRequest;
+import com.google.firebase.internal.NonNull;
+
+/**
+ * Used for looking up an account by phone number.
+ *
+ * @see {FirebaseAuth#getUsers}
+ */
+public final class PhoneIdentifier extends UserIdentifier {
+ private final String phoneNumber;
+
+ public PhoneIdentifier(@NonNull String phoneNumber) {
+ UserRecord.checkPhoneNumber(phoneNumber);
+ this.phoneNumber = phoneNumber;
+ }
+
+ @Override
+ public String toString() {
+ return "PhoneIdentifier(" + phoneNumber + ")";
+ }
+
+ @Override
+ void populate(@NonNull GetAccountInfoRequest payload) {
+ payload.addPhoneNumber(phoneNumber);
+ }
+
+ @Override
+ boolean matches(@NonNull UserRecord userRecord) {
+ return phoneNumber.equals(userRecord.getPhoneNumber());
+ }
+}
diff --git a/src/main/java/com/google/firebase/auth/ProviderIdentifier.java b/src/main/java/com/google/firebase/auth/ProviderIdentifier.java
new file mode 100644
index 000000000..25e00026d
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/ProviderIdentifier.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import com.google.firebase.auth.internal.GetAccountInfoRequest;
+import com.google.firebase.internal.NonNull;
+
+/**
+ * Used for looking up an account by provider.
+ *
+ * @see {FirebaseAuth#getUsers}
+ */
+public final class ProviderIdentifier extends UserIdentifier {
+ private final String providerId;
+ private final String providerUid;
+
+ public ProviderIdentifier(@NonNull String providerId, @NonNull String providerUid) {
+ UserRecord.checkProvider(providerId, providerUid);
+ this.providerId = providerId;
+ this.providerUid = providerUid;
+ }
+
+ @Override
+ public String toString() {
+ return "ProviderIdentifier(" + providerId + ", " + providerUid + ")";
+ }
+
+ @Override
+ void populate(@NonNull GetAccountInfoRequest payload) {
+ payload.addFederatedUserId(providerId, providerUid);
+ }
+
+ @Override
+ boolean matches(@NonNull UserRecord userRecord) {
+ for (UserInfo userInfo : userRecord.getProviderData()) {
+ if (providerId.equals(userInfo.getProviderId()) && providerUid.equals(userInfo.getUid())) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/google/firebase/auth/UidIdentifier.java b/src/main/java/com/google/firebase/auth/UidIdentifier.java
new file mode 100644
index 000000000..a4f7069d9
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/UidIdentifier.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import com.google.firebase.auth.internal.GetAccountInfoRequest;
+import com.google.firebase.internal.NonNull;
+
+/**
+ * Used for looking up an account by uid.
+ *
+ * @see {FirebaseAuth#getUsers}
+ */
+public final class UidIdentifier extends UserIdentifier {
+ private final String uid;
+
+ public UidIdentifier(@NonNull String uid) {
+ UserRecord.checkUid(uid);
+ this.uid = uid;
+ }
+
+ @Override
+ public String toString() {
+ return "UidIdentifier(" + uid + ")";
+ }
+
+ @Override
+ void populate(@NonNull GetAccountInfoRequest payload) {
+ payload.addUid(uid);
+ }
+
+ @Override
+ boolean matches(@NonNull UserRecord userRecord) {
+ return uid.equals(userRecord.getUid());
+ }
+}
diff --git a/src/main/java/com/google/firebase/auth/UserIdentifier.java b/src/main/java/com/google/firebase/auth/UserIdentifier.java
new file mode 100644
index 000000000..7ec9699e6
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/UserIdentifier.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import com.google.firebase.auth.internal.GetAccountInfoRequest;
+import com.google.firebase.internal.NonNull;
+
+/**
+ * Identifies a user to be looked up.
+ */
+public abstract class UserIdentifier {
+ public abstract String toString();
+
+ abstract void populate(@NonNull GetAccountInfoRequest payload);
+
+ abstract boolean matches(@NonNull UserRecord userRecord);
+}
diff --git a/src/main/java/com/google/firebase/auth/UserMetadata.java b/src/main/java/com/google/firebase/auth/UserMetadata.java
index a2872371f..85a24a0fd 100644
--- a/src/main/java/com/google/firebase/auth/UserMetadata.java
+++ b/src/main/java/com/google/firebase/auth/UserMetadata.java
@@ -23,14 +23,16 @@ public class UserMetadata {
private final long creationTimestamp;
private final long lastSignInTimestamp;
+ private final long lastRefreshTimestamp;
public UserMetadata(long creationTimestamp) {
- this(creationTimestamp, 0L);
+ this(creationTimestamp, 0L, 0L);
}
- public UserMetadata(long creationTimestamp, long lastSignInTimestamp) {
+ public UserMetadata(long creationTimestamp, long lastSignInTimestamp, long lastRefreshTimestamp) {
this.creationTimestamp = creationTimestamp;
this.lastSignInTimestamp = lastSignInTimestamp;
+ this.lastRefreshTimestamp = lastRefreshTimestamp;
}
/**
@@ -50,4 +52,13 @@ public long getCreationTimestamp() {
public long getLastSignInTimestamp() {
return lastSignInTimestamp;
}
+
+ /**
+ * Returns the time at which the user was last active (ID token refreshed).
+ *
+ * @return Milliseconds since epoch timestamp, or 0 if the user was never active.
+ */
+ public long getLastRefreshTimestamp() {
+ return lastRefreshTimestamp;
+ }
}
diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java
index e8896de92..0af08f65b 100644
--- a/src/main/java/com/google/firebase/auth/UserRecord.java
+++ b/src/main/java/com/google/firebase/auth/UserRecord.java
@@ -20,6 +20,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.api.client.json.JsonFactory;
+import com.google.api.client.util.DateTime;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -82,7 +83,15 @@ public class UserRecord implements UserInfo {
}
}
this.tokensValidAfterTimestamp = response.getValidSince() * 1000;
- this.userMetadata = new UserMetadata(response.getCreatedAt(), response.getLastLoginAt());
+
+ String lastRefreshAtRfc3339 = response.getLastRefreshAt();
+ long lastRefreshAtMillis = 0;
+ if (!Strings.isNullOrEmpty(lastRefreshAtRfc3339)) {
+ lastRefreshAtMillis = DateTime.parseRfc3339(lastRefreshAtRfc3339).getValue();
+ }
+
+ this.userMetadata = new UserMetadata(
+ response.getCreatedAt(), response.getLastLoginAt(), lastRefreshAtMillis);
this.customClaims = parseCustomClaims(response.getCustomClaims(), jsonFactory);
}
@@ -259,6 +268,11 @@ static void checkPhoneNumber(String phoneNumber) {
"phone number must be a valid, E.164 compliant identifier starting with a '+' sign");
}
+ static void checkProvider(String providerId, String providerUid) {
+ checkArgument(!Strings.isNullOrEmpty(providerId), "providerId must be a non-empty string");
+ checkArgument(!Strings.isNullOrEmpty(providerUid), "providerUid must be a non-empty string");
+ }
+
static void checkUrl(String photoUrl) {
checkArgument(!Strings.isNullOrEmpty(photoUrl), "url cannot be null or empty");
try {
diff --git a/src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java b/src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java
new file mode 100644
index 000000000..728cf6358
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth.internal;
+
+import com.google.api.client.util.Key;
+import java.util.List;
+
+/**
+ * Represents the response from Google identity Toolkit for a batch delete request.
+ */
+public class BatchDeleteResponse {
+
+ @Key("errors")
+ private List errors;
+
+ public List getErrors() {
+ return errors;
+ }
+
+ public static class ErrorInfo {
+ @Key("index")
+ private int index;
+
+ @Key("message")
+ private String message;
+
+ // A 'localId' field also exists here, but is not currently exposed in the Admin SDK.
+
+ public int getIndex() {
+ return index;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+ }
+}
diff --git a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java
new file mode 100644
index 000000000..67c4d0ee7
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth.internal;
+
+import com.google.api.client.util.Key;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents the request to look up account information.
+ */
+public final class GetAccountInfoRequest {
+
+ @Key("localId")
+ private List uids = null;
+
+ @Key("email")
+ private List emails = null;
+
+ @Key("phoneNumber")
+ private List phoneNumbers = null;
+
+ @Key("federatedUserId")
+ private List federatedUserIds = null;
+
+ private static final class FederatedUserId {
+ @Key("providerId")
+ private String providerId = null;
+
+ @Key("rawId")
+ private String rawId = null;
+
+ FederatedUserId(String providerId, String rawId) {
+ this.providerId = providerId;
+ this.rawId = rawId;
+ }
+ }
+
+ public void addUid(String uid) {
+ if (uids == null) {
+ uids = new ArrayList<>();
+ }
+ uids.add(uid);
+ }
+
+ public void addEmail(String email) {
+ if (emails == null) {
+ emails = new ArrayList<>();
+ }
+ emails.add(email);
+ }
+
+ public void addPhoneNumber(String phoneNumber) {
+ if (phoneNumbers == null) {
+ phoneNumbers = new ArrayList<>();
+ }
+ phoneNumbers.add(phoneNumber);
+ }
+
+ public void addFederatedUserId(String providerId, String providerUid) {
+ if (federatedUserIds == null) {
+ federatedUserIds = new ArrayList<>();
+ }
+ federatedUserIds.add(new FederatedUserId(providerId, providerUid));
+ }
+}
diff --git a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java
index 0c7a92e0e..7bde3eb39 100644
--- a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java
+++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java
@@ -76,6 +76,9 @@ public static class User {
@Key("lastLoginAt")
private long lastLoginAt;
+ @Key("lastRefreshAt")
+ private String lastRefreshAt;
+
@Key("validSince")
private long validSince;
@@ -126,6 +129,10 @@ public long getLastLoginAt() {
return lastLoginAt;
}
+ public String getLastRefreshAt() {
+ return lastRefreshAt;
+ }
+
public long getValidSince() {
return validSince;
}
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
index bb9838938..c41772293 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -136,6 +136,78 @@ public void testDeleteNonExistingUser() throws Exception {
}
}
+ @Test
+ public void testDeleteUsers() throws Exception {
+ UserRecord user1 = newUserWithParams();
+ UserRecord user2 = newUserWithParams();
+ UserRecord user3 = newUserWithParams();
+
+ DeleteUsersResult deleteUsersResult =
+ slowDeleteUsersAsync(ImmutableList.of(user1.getUid(), user2.getUid(), user3.getUid()))
+ .get();
+
+ assertEquals(3, deleteUsersResult.getSuccessCount());
+ assertEquals(0, deleteUsersResult.getFailureCount());
+ assertTrue(deleteUsersResult.getErrors().isEmpty());
+
+ GetUsersResult getUsersResult =
+ auth.getUsersAsync(
+ ImmutableList.of(new UidIdentifier(user1.getUid()),
+ new UidIdentifier(user2.getUid()), new UidIdentifier(user3.getUid())))
+ .get();
+
+ assertTrue(getUsersResult.getUsers().isEmpty());
+ assertEquals(3, getUsersResult.getNotFound().size());
+ }
+
+ @Test
+ public void testDeleteExistingAndNonExistingUsers() throws Exception {
+ UserRecord user1 = newUserWithParams();
+
+ DeleteUsersResult deleteUsersResult =
+ slowDeleteUsersAsync(ImmutableList.of(user1.getUid(), "uid-that-doesnt-exist")).get();
+
+ assertEquals(2, deleteUsersResult.getSuccessCount());
+ assertEquals(0, deleteUsersResult.getFailureCount());
+ assertTrue(deleteUsersResult.getErrors().isEmpty());
+
+ GetUsersResult getUsersResult =
+ auth.getUsersAsync(ImmutableList.of(new UidIdentifier(user1.getUid()),
+ new UidIdentifier("uid-that-doesnt-exist")))
+ .get();
+
+ assertTrue(getUsersResult.getUsers().isEmpty());
+ assertEquals(2, getUsersResult.getNotFound().size());
+ }
+
+ @Test
+ public void testDeleteUsersIsIdempotent() throws Exception {
+ UserRecord user1 = newUserWithParams();
+
+ DeleteUsersResult result = slowDeleteUsersAsync(ImmutableList.of(user1.getUid())).get();
+
+ assertEquals(1, result.getSuccessCount());
+ assertEquals(0, result.getFailureCount());
+ assertTrue(result.getErrors().isEmpty());
+
+ // Delete the user again to ensure that everything still counts as a success.
+ result = slowDeleteUsersAsync(ImmutableList.of(user1.getUid())).get();
+
+ assertEquals(1, result.getSuccessCount());
+ assertEquals(0, result.getFailureCount());
+ assertTrue(result.getErrors().isEmpty());
+ }
+
+ /**
+ * The {@code batchDelete} endpoint has a rate limit of 1 QPS. Use this test
+ * helper to ensure you don't exceed the quota.
+ */
+ // TODO(rsgowman): When/if the rate limit is relaxed, eliminate this helper.
+ private ApiFuture slowDeleteUsersAsync(List uids) throws Exception {
+ TimeUnit.SECONDS.sleep(1);
+ return auth.deleteUsersAsync(uids);
+ }
+
@Test
public void testCreateUserWithParams() throws Exception {
RandomUser randomUser = RandomUser.create();
@@ -243,6 +315,35 @@ public void testUserLifecycle() throws Exception {
assertUserDoesNotExist(auth, userRecord.getUid());
}
+ @Test
+ public void testLastRefreshTime() throws Exception {
+ RandomUser user = RandomUser.create();
+ UserRecord newUserRecord = auth.createUser(new UserRecord.CreateRequest()
+ .setUid(user.uid)
+ .setEmail(user.email)
+ .setEmailVerified(false)
+ .setPassword("password"));
+
+ try {
+ // New users should not have a lastRefreshTimestamp set.
+ assertEquals(0, newUserRecord.getUserMetadata().getLastRefreshTimestamp());
+
+ // Login to cause the lastRefreshTimestamp to be set.
+ signInWithPassword(newUserRecord.getEmail(), "password");
+
+ UserRecord userRecord = auth.getUser(newUserRecord.getUid());
+
+ // Ensure the lastRefreshTimestamp is approximately "now" (with a tollerance of 10 minutes).
+ long now = System.currentTimeMillis();
+ long tollerance = TimeUnit.MINUTES.toMillis(10);
+ long lastRefreshTimestamp = userRecord.getUserMetadata().getLastRefreshTimestamp();
+ assertTrue(now - tollerance <= lastRefreshTimestamp);
+ assertTrue(lastRefreshTimestamp <= now + tollerance);
+ } finally {
+ auth.deleteUser(newUserRecord.getUid());
+ }
+ }
+
@Test
public void testListUsers() throws Exception {
final List uids = new ArrayList<>();
@@ -974,7 +1075,7 @@ private Map parseLinkParameters(String link) throws Exception {
return result;
}
- private String randomPhoneNumber() {
+ static String randomPhoneNumber() {
Random random = new Random();
StringBuilder builder = new StringBuilder("+1");
for (int i = 0; i < 10; i++) {
@@ -1013,7 +1114,7 @@ private String signInWithPassword(String email, String password) throws IOExcept
GenericUrl url = new GenericUrl(VERIFY_PASSWORD_URL + "?key="
+ IntegrationTestUtils.getApiKey());
Map content = ImmutableMap.of(
- "email", email, "password", password);
+ "email", email, "password", password, "returnSecureToken", true);
HttpRequest request = transport.createRequestFactory().buildPostRequest(url,
new JsonHttpContent(jsonFactory, content));
request.setParser(new JsonObjectParser(jsonFactory));
@@ -1072,9 +1173,9 @@ private void checkRecreateUser(String uid) throws Exception {
}
}
- private static class RandomUser {
- private final String uid;
- private final String email;
+ static class RandomUser {
+ final String uid;
+ final String email;
private RandomUser(String uid, String email) {
this.uid = uid;
@@ -1100,4 +1201,21 @@ private static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, St
((FirebaseAuthException) e.getCause()).getErrorCode());
}
}
+
+ static UserRecord newUserWithParams() throws Exception {
+ return newUserWithParams(auth);
+ }
+
+ static UserRecord newUserWithParams(FirebaseAuth auth) throws Exception {
+ // TODO(rsgowman): This function could be used throughout this file (similar to the other
+ // ports).
+ RandomUser randomUser = RandomUser.create();
+ return auth.createUser(new UserRecord.CreateRequest()
+ .setUid(randomUser.uid)
+ .setEmail(randomUser.email)
+ .setPhoneNumber(randomPhoneNumber())
+ .setDisplayName("Random User")
+ .setPhotoUrl("https://example.com/photo.png")
+ .setPassword("password"));
+ }
}
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index 20eb56222..ac8860c72 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -35,6 +35,7 @@
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
import com.google.auth.oauth2.GoogleCredentials;
+import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -52,6 +53,8 @@
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -170,6 +173,153 @@ public void testGetUserByPhoneNumberWithNotFoundError() throws Exception {
}
}
+ @Test
+ public void testGetUsersExceeds100() throws Exception {
+ FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+ .setCredentials(credentials)
+ .build());
+ List identifiers = new ArrayList<>();
+ for (int i = 0; i < 101; i++) {
+ identifiers.add(new UidIdentifier("uid_" + i));
+ }
+
+ try {
+ FirebaseAuth.getInstance().getUsers(identifiers);
+ fail("No error thrown for too many supplied identifiers");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testGetUsersNull() throws Exception {
+ FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+ .setCredentials(credentials)
+ .build());
+ try {
+ FirebaseAuth.getInstance().getUsers(null);
+ fail("No error thrown for null identifiers");
+ } catch (NullPointerException expected) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testGetUsersEmpty() throws Exception {
+ initializeAppForUserManagement();
+ GetUsersResult result = FirebaseAuth.getInstance().getUsers(new ArrayList());
+ assertTrue(result.getUsers().isEmpty());
+ assertTrue(result.getNotFound().isEmpty());
+ }
+
+ @Test
+ public void testGetUsersAllNonExisting() throws Exception {
+ initializeAppForUserManagement("{ \"users\": [] }");
+ List ids = ImmutableList.of(
+ new UidIdentifier("id-that-doesnt-exist"));
+ GetUsersResult result = FirebaseAuth.getInstance().getUsers(ids);
+ assertTrue(result.getUsers().isEmpty());
+ assertEquals(ids.size(), result.getNotFound().size());
+ assertTrue(result.getNotFound().containsAll(ids));
+ }
+
+ @Test
+ public void testGetUsersMultipleIdentifierTypes() throws Exception {
+ initializeAppForUserManagement((""
+ + "{ "
+ + " 'users': [{ "
+ + " 'localId': 'uid1', "
+ + " 'email': 'user1@example.com', "
+ + " 'phoneNumber': '+15555550001' "
+ + " }, { "
+ + " 'localId': 'uid2', "
+ + " 'email': 'user2@example.com', "
+ + " 'phoneNumber': '+15555550002' "
+ + " }, { "
+ + " 'localId': 'uid3', "
+ + " 'email': 'user3@example.com', "
+ + " 'phoneNumber': '+15555550003' "
+ + " }, { "
+ + " 'localId': 'uid4', "
+ + " 'email': 'user4@example.com', "
+ + " 'phoneNumber': '+15555550004', "
+ + " 'providerUserInfo': [{ "
+ + " 'providerId': 'google.com', "
+ + " 'rawId': 'google_uid4' "
+ + " }] "
+ + " }] "
+ + "} "
+ ).replace("'", "\""));
+
+ UidIdentifier doesntExist = new UidIdentifier("this-uid-doesnt-exist");
+ List ids = ImmutableList.of(
+ new UidIdentifier("uid1"),
+ new EmailIdentifier("user2@example.com"),
+ new PhoneIdentifier("+15555550003"),
+ new ProviderIdentifier("google.com", "google_uid4"),
+ doesntExist);
+ GetUsersResult result = FirebaseAuth.getInstance().getUsers(ids);
+ Collection uids = userRecordsToUids(result.getUsers());
+ assertTrue(uids.containsAll(ImmutableList.of("uid1", "uid2", "uid3", "uid4")));
+ assertEquals(1, result.getNotFound().size());
+ assertTrue(result.getNotFound().contains(doesntExist));
+ }
+
+ private Collection userRecordsToUids(Collection userRecords) {
+ Collection uids = new HashSet<>();
+ for (UserRecord userRecord : userRecords) {
+ uids.add(userRecord.getUid());
+ }
+ return uids;
+ }
+
+ @Test
+ public void testInvalidUidIdentifier() throws Exception {
+ try {
+ new UidIdentifier("too long " + Strings.repeat(".", 128));
+ fail("No error thrown for invalid uid");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testInvalidEmailIdentifier() throws Exception {
+ try {
+ new EmailIdentifier("invalid email addr");
+ fail("No error thrown for invalid email");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testInvalidPhoneIdentifier() throws Exception {
+ try {
+ new PhoneIdentifier("invalid phone number");
+ fail("No error thrown for invalid phone number");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testInvalidProviderIdentifier() throws Exception {
+ try {
+ new ProviderIdentifier("", "valid-uid");
+ fail("No error thrown for invalid provider id");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+
+ try {
+ new ProviderIdentifier("valid-id", "");
+ fail("No error thrown for invalid provider uid");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+ }
+
@Test
public void testListUsers() throws Exception {
final TestResponseInterceptor interceptor = initializeAppForUserManagement(
@@ -276,6 +426,81 @@ public void testDeleteUser() throws Exception {
checkRequestHeaders(interceptor);
}
+ @Test
+ public void testDeleteUsersExceeds1000() throws Exception {
+ FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+ .setCredentials(credentials)
+ .build());
+ List ids = new ArrayList<>();
+ for (int i = 0; i < 1001; i++) {
+ ids.add("id" + i);
+ }
+ try {
+ FirebaseAuth.getInstance().deleteUsersAsync(ids);
+ fail("No error thrown for too many uids");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testDeleteUsersInvalidId() throws Exception {
+ FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+ .setCredentials(credentials)
+ .build());
+ try {
+ FirebaseAuth.getInstance().deleteUsersAsync(
+ ImmutableList.of("too long " + Strings.repeat(".", 128)));
+ fail("No error thrown for too long uid");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testDeleteUsersIndexesErrorsCorrectly() throws Exception {
+ initializeAppForUserManagement((""
+ + "{ "
+ + " 'errors': [{ "
+ + " 'index': 0, "
+ + " 'localId': 'uid1', "
+ + " 'message': 'NOT_DISABLED : Disable the account before batch deletion.' "
+ + " }, { "
+ + " 'index': 2, "
+ + " 'localId': 'uid3', "
+ + " 'message': 'something awful' "
+ + " }] "
+ + "} "
+ ).replace("'", "\""));
+
+ DeleteUsersResult result = FirebaseAuth.getInstance().deleteUsersAsync(ImmutableList.of(
+ "uid1", "uid2", "uid3", "uid4"
+ )).get();
+
+ assertEquals(2, result.getSuccessCount());
+ assertEquals(2, result.getFailureCount());
+ assertEquals(2, result.getErrors().size());
+ assertEquals(0, result.getErrors().get(0).getIndex());
+ assertEquals(
+ "NOT_DISABLED : Disable the account before batch deletion.",
+ result.getErrors().get(0).getReason());
+ assertEquals(2, result.getErrors().get(1).getIndex());
+ assertEquals("something awful", result.getErrors().get(1).getReason());
+ }
+
+ @Test
+ public void testDeleteUsersSuccess() throws Exception {
+ initializeAppForUserManagement("{}");
+
+ DeleteUsersResult result = FirebaseAuth.getInstance().deleteUsersAsync(ImmutableList.of(
+ "uid1", "uid2", "uid3"
+ )).get();
+
+ assertEquals(3, result.getSuccessCount());
+ assertEquals(0, result.getFailureCount());
+ assertTrue(result.getErrors().isEmpty());
+ }
+
@Test
public void testImportUsers() throws Exception {
TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
diff --git a/src/test/java/com/google/firebase/auth/GetUsersIT.java b/src/test/java/com/google/firebase/auth/GetUsersIT.java
new file mode 100644
index 000000000..efe2f783f
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/GetUsersIT.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.testing.IntegrationTestUtils;
+import java.util.Collection;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class GetUsersIT {
+ private static FirebaseAuth auth;
+ private static UserRecord testUser1;
+ private static UserRecord testUser2;
+ private static UserRecord testUser3;
+ private static String importUserUid;
+
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp();
+ auth = FirebaseAuth.getInstance(masterApp);
+
+ testUser1 = FirebaseAuthIT.newUserWithParams(auth);
+ testUser2 = FirebaseAuthIT.newUserWithParams(auth);
+ testUser3 = FirebaseAuthIT.newUserWithParams(auth);
+
+ FirebaseAuthIT.RandomUser randomUser = FirebaseAuthIT.RandomUser.create();
+ importUserUid = randomUser.uid;
+ String phone = FirebaseAuthIT.randomPhoneNumber();
+ UserImportResult result = auth.importUsers(ImmutableList.of(
+ ImportUserRecord.builder()
+ .setUid(randomUser.uid)
+ .setEmail(randomUser.email)
+ .setPhoneNumber(phone)
+ .addUserProvider(
+ UserProvider.builder()
+ .setProviderId("google.com")
+ .setUid("google_" + randomUser.uid)
+ .build())
+ .build()
+ ));
+ assertEquals(1, result.getSuccessCount());
+ assertEquals(0, result.getFailureCount());
+ }
+
+ @AfterClass
+ public static void cleanup() throws Exception {
+ // TODO(rsgowman): deleteUsers (plural) would make more sense here, but it's currently rate
+ // limited to 1qps. When/if that's relaxed, change this to just delete them all at once.
+ auth.deleteUser(testUser1.getUid());
+ auth.deleteUser(testUser2.getUid());
+ auth.deleteUser(testUser3.getUid());
+ auth.deleteUser(importUserUid);
+ }
+
+ @Test
+ public void testVariousIdentifiers() throws Exception {
+ GetUsersResult result = auth.getUsersAsync(ImmutableList.of(
+ new UidIdentifier(testUser1.getUid()),
+ new EmailIdentifier(testUser2.getEmail()),
+ new PhoneIdentifier(testUser3.getPhoneNumber()),
+ new ProviderIdentifier("google.com", "google_" + importUserUid)
+ )).get();
+
+ Collection expectedUids = ImmutableList.of(
+ testUser1.getUid(), testUser2.getUid(), testUser3.getUid(), importUserUid);
+
+ assertTrue(sameUsers(result.getUsers(), expectedUids));
+ assertEquals(0, result.getNotFound().size());
+ }
+
+ @Test
+ public void testIgnoresNonExistingUsers() throws Exception {
+ UidIdentifier doesntExistId = new UidIdentifier("uid_that_doesnt_exist");
+ GetUsersResult result = auth.getUsersAsync(ImmutableList.of(
+ new UidIdentifier(testUser1.getUid()),
+ doesntExistId,
+ new UidIdentifier(testUser3.getUid())
+ )).get();
+
+ Collection expectedUids = ImmutableList.of(testUser1.getUid(), testUser3.getUid());
+
+ assertTrue(sameUsers(result.getUsers(), expectedUids));
+ assertEquals(1, result.getNotFound().size());
+ assertTrue(result.getNotFound().contains(doesntExistId));
+ }
+
+ @Test
+ public void testOnlyNonExistingUsers() throws Exception {
+ UidIdentifier doesntExistId = new UidIdentifier("uid_that_doesnt_exist");
+ GetUsersResult result = auth.getUsersAsync(ImmutableList.of(
+ doesntExistId
+ )).get();
+
+ assertEquals(0, result.getUsers().size());
+ assertEquals(1, result.getNotFound().size());
+ assertTrue(result.getNotFound().contains(doesntExistId));
+ }
+
+ @Test
+ public void testDedupsDuplicateUsers() throws Exception {
+ GetUsersResult result = auth.getUsersAsync(ImmutableList.of(
+ new UidIdentifier(testUser1.getUid()),
+ new UidIdentifier(testUser1.getUid())
+ )).get();
+
+ Collection expectedUids = ImmutableList.of(testUser1.getUid());
+
+ assertEquals(1, result.getUsers().size());
+ assertTrue(sameUsers(result.getUsers(), expectedUids));
+ assertEquals(0, result.getNotFound().size());
+ }
+
+ /**
+ * Checks to see if the userRecords collection contains the given uids.
+ *
+ * Behaviour is undefined if there are duplicate entries in either of the parameters.
+ */
+ private boolean sameUsers(Collection userRecords, Collection uids) {
+ if (userRecords.size() != uids.size()) {
+ return false;
+ }
+
+ for (UserRecord userRecord : userRecords) {
+ if (!uids.contains(userRecord.getUid())) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java b/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java
index 011a5cc04..e2ae36c09 100644
--- a/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java
+++ b/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java
@@ -62,7 +62,7 @@ public void testAllProperties() throws IOException {
.setDisplayName("Test User")
.setPhotoUrl("https://test.com/user.png")
.setPhoneNumber("+1234567890")
- .setUserMetadata(new UserMetadata(date.getTime(), date.getTime()))
+ .setUserMetadata(new UserMetadata(date.getTime(), date.getTime(), date.getTime()))
.setDisabled(false)
.setEmailVerified(true)
.setPasswordHash("password".getBytes())