diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index 8298499b4..21abdeaf3 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -42,8 +42,8 @@
import com.google.firebase.auth.UserRecord.UpdateRequest;
import com.google.firebase.auth.internal.DownloadAccountResponse;
import com.google.firebase.auth.internal.GetAccountInfoResponse;
-
import com.google.firebase.auth.internal.HttpErrorResponse;
+import com.google.firebase.auth.internal.ListTenantsResponse;
import com.google.firebase.auth.internal.UploadAccountResponse;
import com.google.firebase.internal.FirebaseRequestInitializer;
import com.google.firebase.internal.NonNull;
@@ -57,6 +57,9 @@
/**
* FirebaseUserManager provides methods for interacting with the Google Identity Toolkit via its
* REST API. This class does not hold any mutable state, and is thread safe.
+ *
+ *
TODO(micahstairs): Consider renaming this to FirebaseAuthManager since this also supports
+ * tenants.
*
* @see
* Google Identity Toolkit
@@ -87,6 +90,7 @@ class FirebaseUserManager {
.put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain")
.build();
+ static final int MAX_LIST_TENANTS_RESULTS = 1000;
static final int MAX_LIST_USERS_RESULTS = 1000;
static final int MAX_IMPORT_USERS = 1000;
@@ -95,10 +99,11 @@ class FirebaseUserManager {
"iss", "jti", "nbf", "nonce", "sub", "firebase");
private static final String ID_TOOLKIT_URL =
- "https://identitytoolkit.googleapis.com/v1/projects/%s";
+ "https://identitytoolkit.googleapis.com/%s/projects/%s";
private static final String CLIENT_VERSION_HEADER = "X-Client-Version";
- private final String baseUrl;
+ private final String userMgtBaseUrl;
+ private final String tenantMgtBaseUrl;
private final JsonFactory jsonFactory;
private final HttpRequestFactory requestFactory;
private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion();
@@ -117,7 +122,8 @@ class FirebaseUserManager {
"Project ID is required to access the auth service. Use a service account credential or "
+ "set the project ID explicitly via FirebaseOptions. Alternatively you can also "
+ "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.");
- this.baseUrl = String.format(ID_TOOLKIT_URL, projectId);
+ this.userMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v1", projectId);
+ this.tenantMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v2", projectId);
this.jsonFactory = app.getOptions().getJsonFactory();
HttpTransport transport = app.getOptions().getHttpTransport();
this.requestFactory = transport.createRequestFactory(new FirebaseRequestInitializer(app));
@@ -201,7 +207,7 @@ DownloadAccountResponse listUsers(int maxResults, String pageToken) throws Fireb
builder.put("nextPageToken", pageToken);
}
- GenericUrl url = new GenericUrl(baseUrl + "/accounts:batchGet");
+ GenericUrl url = new GenericUrl(userMgtBaseUrl + "/accounts:batchGet");
url.putAll(builder.build());
DownloadAccountResponse response = sendRequest(
"GET", url, null, DownloadAccountResponse.class);
@@ -221,6 +227,24 @@ UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthExcep
return new UserImportResult(request.getUsersCount(), response);
}
+ ListTenantsResponse listTenants(int maxResults, String pageToken) throws FirebaseAuthException {
+ ImmutableMap.Builder builder = ImmutableMap.builder()
+ .put("pageSize", maxResults);
+ if (pageToken != null) {
+ checkArgument(!pageToken.equals(
+ ListTenantsPage.END_OF_LIST), "invalid end of list page token");
+ builder.put("pageToken", pageToken);
+ }
+
+ GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants:list");
+ url.putAll(builder.build());
+ ListTenantsResponse response = sendRequest("GET", url, null, ListTenantsResponse.class);
+ if (response == null) {
+ throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve tenants.");
+ }
+ return response;
+ }
+
String createSessionCookie(String idToken,
SessionCookieOptions options) throws FirebaseAuthException {
final Map payload = ImmutableMap.of(
@@ -257,7 +281,7 @@ String getEmailActionLink(EmailLinkType type, String email,
private T post(String path, Object content, Class clazz) throws FirebaseAuthException {
checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty");
checkNotNull(content, "content must not be null for POST requests");
- GenericUrl url = new GenericUrl(baseUrl + path);
+ GenericUrl url = new GenericUrl(userMgtBaseUrl + path);
return sendRequest("POST", url, content, clazz);
}
diff --git a/src/main/java/com/google/firebase/auth/ListTenantsPage.java b/src/main/java/com/google/firebase/auth/ListTenantsPage.java
new file mode 100644
index 000000000..5f5f213fd
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/ListTenantsPage.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.api.client.json.JsonFactory;
+import com.google.api.gax.paging.Page;
+import com.google.common.collect.ImmutableList;
+import com.google.firebase.auth.internal.DownloadAccountResponse;
+import com.google.firebase.auth.internal.ListTenantsResponse;
+import com.google.firebase.internal.NonNull;
+import com.google.firebase.internal.Nullable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+/**
+ * Represents a page of {@link Tenant} instances.
+ *
+ * Provides methods for iterating over the tenants in the current page, and calling up
+ * subsequent pages of tenants.
+ *
+ *
Instances of this class are thread-safe and immutable.
+ */
+public class ListTenantsPage implements Page {
+
+ static final String END_OF_LIST = "";
+
+ private final ListTenantsResponse currentBatch;
+ private final TenantSource source;
+ private final int maxResults;
+
+ private ListTenantsPage(
+ @NonNull ListTenantsResponse currentBatch, @NonNull TenantSource source, int maxResults) {
+ this.currentBatch = checkNotNull(currentBatch);
+ this.source = checkNotNull(source);
+ this.maxResults = maxResults;
+ }
+
+ /**
+ * Checks if there is another page of tenants available to retrieve.
+ *
+ * @return true if another page is available, or false otherwise.
+ */
+ @Override
+ public boolean hasNextPage() {
+ return !END_OF_LIST.equals(currentBatch.getPageToken());
+ }
+
+ /**
+ * Returns the string token that identifies the next page.
+ *
+ * Never returns null. Returns empty string if there are no more pages available to be
+ * retrieved.
+ *
+ * @return A non-null string token (possibly empty, representing no more pages)
+ */
+ @NonNull
+ @Override
+ public String getNextPageToken() {
+ return currentBatch.getPageToken();
+ }
+
+ /**
+ * Returns the next page of tenants.
+ *
+ * @return A new {@link ListTenantsPage} instance, or null if there are no more pages.
+ */
+ @Nullable
+ @Override
+ public ListTenantsPage getNextPage() {
+ if (hasNextPage()) {
+ PageFactory factory = new PageFactory(source, maxResults, currentBatch.getPageToken());
+ try {
+ return factory.create();
+ } catch (FirebaseAuthException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns an {@link Iterable} that facilitates transparently iterating over all the tenants in
+ * the current Firebase project, starting from this page.
+ *
+ *
The {@link Iterator} instances produced by the returned {@link Iterable} never buffers more
+ * than one page of tenants at a time. It is safe to abandon the iterators (i.e. break the loops)
+ * at any time.
+ *
+ * @return a new {@link Iterable} instance.
+ */
+ @NonNull
+ @Override
+ public Iterable iterateAll() {
+ return new TenantIterable(this);
+ }
+
+ /**
+ * Returns an {@code Iterable} over the users in this page.
+ *
+ * @return a {@code Iterable} instance.
+ */
+ @NonNull
+ @Override
+ public Iterable getValues() {
+ return currentBatch.getTenants();
+ }
+
+ private static class TenantIterable implements Iterable {
+
+ private final ListTenantsPage startingPage;
+
+ TenantIterable(@NonNull ListTenantsPage startingPage) {
+ this.startingPage = checkNotNull(startingPage, "starting page must not be null");
+ }
+
+ @Override
+ @NonNull
+ public Iterator iterator() {
+ return new TenantIterator(startingPage);
+ }
+
+ /**
+ * An {@link Iterator} that cycles through tenants, one at a time.
+ *
+ * It buffers the last retrieved batch of tenants in memory. The {@code maxResults} parameter
+ * is an upper bound on the batch size.
+ */
+ private static class TenantIterator implements Iterator {
+
+ private ListTenantsPage currentPage;
+ private List batch;
+ private int index = 0;
+
+ private TenantIterator(ListTenantsPage startingPage) {
+ setCurrentPage(startingPage);
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (index == batch.size()) {
+ if (currentPage.hasNextPage()) {
+ setCurrentPage(currentPage.getNextPage());
+ } else {
+ return false;
+ }
+ }
+
+ return index < batch.size();
+ }
+
+ @Override
+ public Tenant next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ return batch.get(index++);
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("remove operation not supported");
+ }
+
+ private void setCurrentPage(ListTenantsPage page) {
+ this.currentPage = checkNotNull(page);
+ this.batch = ImmutableList.copyOf(page.getValues());
+ this.index = 0;
+ }
+ }
+ }
+
+ /**
+ * Represents a source of tenant data that can be queried to load a batch of tenants.
+ */
+ interface TenantSource {
+ @NonNull
+ ListTenantsResponse fetch(int maxResults, String pageToken)
+ throws FirebaseAuthException;
+ }
+
+ static class DefaultTenantSource implements TenantSource {
+
+ private final FirebaseUserManager userManager;
+ private final JsonFactory jsonFactory;
+
+ DefaultTenantSource(FirebaseUserManager userManager, JsonFactory jsonFactory) {
+ this.userManager = checkNotNull(userManager, "user manager must not be null");
+ this.jsonFactory = checkNotNull(jsonFactory, "json factory must not be null");
+ }
+
+ @Override
+ public ListTenantsResponse fetch(int maxResults, String pageToken)
+ throws FirebaseAuthException {
+ return userManager.listTenants(maxResults, pageToken);
+ }
+ }
+
+ /**
+ * A simple factory class for {@link ListTenantsPage} instances.
+ *
+ * Performs argument validation before attempting to load any tenant data (which is expensive,
+ * and hence may be performed asynchronously on a separate thread).
+ */
+ static class PageFactory {
+
+ private final TenantSource source;
+ private final int maxResults;
+ private final String pageToken;
+
+ PageFactory(@NonNull TenantSource source) {
+ this(source, FirebaseUserManager.MAX_LIST_TENANTS_RESULTS, null);
+ }
+
+ PageFactory(@NonNull TenantSource source, int maxResults, @Nullable String pageToken) {
+ checkArgument(maxResults > 0 && maxResults <= FirebaseUserManager.MAX_LIST_TENANTS_RESULTS,
+ "maxResults must be a positive integer that does not exceed %s",
+ FirebaseUserManager.MAX_LIST_TENANTS_RESULTS);
+ checkArgument(!END_OF_LIST.equals(pageToken), "invalid end of list page token");
+ this.source = checkNotNull(source, "source must not be null");
+ this.maxResults = maxResults;
+ this.pageToken = pageToken;
+ }
+
+ ListTenantsPage create() throws FirebaseAuthException {
+ ListTenantsResponse batch = source.fetch(maxResults, pageToken);
+ return new ListTenantsPage(batch, source, maxResults);
+ }
+ }
+}
+
diff --git a/src/main/java/com/google/firebase/auth/Tenant.java b/src/main/java/com/google/firebase/auth/Tenant.java
index a3e1471a3..f64d93a78 100644
--- a/src/main/java/com/google/firebase/auth/Tenant.java
+++ b/src/main/java/com/google/firebase/auth/Tenant.java
@@ -33,10 +33,26 @@ public final class Tenant {
private String displayName;
@Key("allowPasswordSignup")
- private String passwordSignInAllowed;
+ private boolean passwordSignInAllowed;
@Key("enableEmailLinkSignin")
- private String emailLinkSignInEnabled;
+ private boolean emailLinkSignInEnabled;
+
+ public String getTenantId() {
+ return tenantId;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public boolean isPasswordSignInAllowed() {
+ return passwordSignInAllowed;
+ }
+
+ public boolean isEmailLinkSignInEnabled() {
+ return emailLinkSignInEnabled;
+ }
/**
* Class used to hold the information needs to make a tenant create request.
@@ -73,8 +89,8 @@ public static Builder newBuilder() {
}
/**
- * Builder class used to construct a create request.
- */
+ * Builder class used to construct a create request.
+ */
@AutoValue.Builder
abstract static class Builder {
public abstract Builder setDisplayName(String displayName);
@@ -122,8 +138,8 @@ public static Builder newBuilder() {
}
/**
- * Builder class used to construct a update request.
- */
+ * Builder class used to construct a update request.
+ */
@AutoValue.Builder
abstract static class Builder {
public abstract Builder setDisplayName(String displayName);
diff --git a/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java
new file mode 100644
index 000000000..7abb79805
--- /dev/null
+++ b/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth.internal;
+
+import com.google.api.client.util.Key;
+import com.google.firebase.auth.Tenant;
+import java.util.List;
+
+/**
+ * JSON data binding for ListTenantsResponse messages sent by Google identity toolkit service.
+ */
+public class ListTenantsResponse {
+
+ @Key("tenants")
+ private List tenants;
+
+ @Key("pageToken")
+ private String pageToken;
+
+ public ListTenantsResponse(List tenants, String pageToken) {
+ this.tenants = tenants;
+ this.pageToken = pageToken;
+ }
+
+ public ListTenantsResponse() { }
+
+ public List getTenants() {
+ return tenants;
+ }
+
+ public boolean hasTenants() {
+ return tenants != null && !tenants.isEmpty();
+ }
+
+ public String getPageToken() {
+ return pageToken;
+ }
+}
diff --git a/src/test/java/com/google/firebase/auth/ListTenantsPageTest.java b/src/test/java/com/google/firebase/auth/ListTenantsPageTest.java
new file mode 100644
index 000000000..227e3dca4
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/ListTenantsPageTest.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.auth;
+
+import static junit.framework.TestCase.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import com.google.api.client.googleapis.util.Utils;
+import com.google.api.client.json.JsonFactory;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.BaseEncoding;
+import com.google.firebase.auth.internal.ListTenantsResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.junit.Test;
+
+public class ListTenantsPageTest {
+
+ @Test
+ public void testSinglePage() throws FirebaseAuthException, IOException {
+ TestTenantSource source = new TestTenantSource(3);
+ ListTenantsPage page = new ListTenantsPage.PageFactory(source).create();
+ assertFalse(page.hasNextPage());
+ assertEquals(ListTenantsPage.END_OF_LIST, page.getNextPageToken());
+ assertNull(page.getNextPage());
+
+ ImmutableList tenants = ImmutableList.copyOf(page.getValues());
+ assertEquals(3, tenants.size());
+ for (int i = 0; i < 3; i++) {
+ assertEquals("tenant" + i, tenants.get(i).getTenantId());
+ }
+ assertEquals(1, source.calls.size());
+ assertNull(source.calls.get(0));
+ }
+
+ @Test
+ public void testMultiplePages() throws FirebaseAuthException, IOException {
+ ListTenantsResponse response = new ListTenantsResponse(
+ ImmutableList.of(newTenant("tenant0"), newTenant("tenant1"), newTenant("tenant2")),
+ "token");
+ TestTenantSource source = new TestTenantSource(response);
+ ListTenantsPage page1 = new ListTenantsPage.PageFactory(source).create();
+ assertTrue(page1.hasNextPage());
+ assertEquals("token", page1.getNextPageToken());
+ ImmutableList tenants = ImmutableList.copyOf(page1.getValues());
+ assertEquals(3, tenants.size());
+ for (int i = 0; i < 3; i++) {
+ assertEquals("tenant" + i, tenants.get(i).getTenantId());
+ }
+
+ response = new ListTenantsResponse(
+ ImmutableList.of(newTenant("tenant3"), newTenant("tenant4"), newTenant("tenant5")),
+ ListTenantsPage.END_OF_LIST);
+ source.response = response;
+ ListTenantsPage page2 = page1.getNextPage();
+ assertFalse(page2.hasNextPage());
+ assertEquals(ListTenantsPage.END_OF_LIST, page2.getNextPageToken());
+ tenants = ImmutableList.copyOf(page2.getValues());
+ assertEquals(3, tenants.size());
+ for (int i = 3; i < 6; i++) {
+ assertEquals("tenant" + i, tenants.get(i - 3).getTenantId());
+ }
+
+ assertEquals(2, source.calls.size());
+ assertNull(source.calls.get(0));
+ assertEquals("token", source.calls.get(1));
+
+ // Should iterate all tenants from both pages
+ int iterations = 0;
+ for (Tenant tenant : page1.iterateAll()) {
+ iterations++;
+ }
+ assertEquals(6, iterations);
+ assertEquals(3, source.calls.size());
+ assertEquals("token", source.calls.get(2));
+
+ // Should only iterate tenants in the last page
+ iterations = 0;
+ for (Tenant tenant : page2.iterateAll()) {
+ iterations++;
+ }
+ assertEquals(3, iterations);
+ assertEquals(3, source.calls.size());
+ }
+
+ @Test
+ public void testListTenantsIterable() throws FirebaseAuthException, IOException {
+ TestTenantSource source = new TestTenantSource(3);
+ ListTenantsPage page = new ListTenantsPage.PageFactory(source).create();
+ Iterable tenants = page.iterateAll();
+
+ int iterations = 0;
+ for (Tenant tenant : tenants) {
+ assertEquals("tenant" + iterations, tenant.getTenantId());
+ iterations++;
+ }
+ assertEquals(3, iterations);
+ assertEquals(1, source.calls.size());
+ assertNull(source.calls.get(0));
+
+ // Should result in a new iterator
+ iterations = 0;
+ for (Tenant tenant : tenants) {
+ assertEquals("tenant" + iterations, tenant.getTenantId());
+ iterations++;
+ }
+ assertEquals(3, iterations);
+ assertEquals(1, source.calls.size());
+ assertNull(source.calls.get(0));
+ }
+
+ @Test
+ public void testListTenantsIterator() throws FirebaseAuthException, IOException {
+ TestTenantSource source = new TestTenantSource(3);
+ ListTenantsPage page = new ListTenantsPage.PageFactory(source).create();
+ Iterable tenants = page.iterateAll();
+ Iterator iterator = tenants.iterator();
+ int iterations = 0;
+ while (iterator.hasNext()) {
+ assertEquals("tenant" + iterations, iterator.next().getTenantId());
+ iterations++;
+ }
+ assertEquals(3, iterations);
+ assertEquals(1, source.calls.size());
+ assertNull(source.calls.get(0));
+
+ while (iterator.hasNext()) {
+ fail("Should not be able to to iterate any more");
+ }
+ try {
+ iterator.next();
+ fail("Should not be able to iterate any more");
+ } catch (NoSuchElementException expected) {
+ // expected
+ }
+ assertEquals(1, source.calls.size());
+ }
+
+ @Test
+ public void testListTenantsPagedIterable() throws FirebaseAuthException, IOException {
+ ListTenantsResponse response = new ListTenantsResponse(
+ ImmutableList.of(newTenant("tenant0"), newTenant("tenant1"), newTenant("tenant2")),
+ "token");
+ TestTenantSource source = new TestTenantSource(response);
+ ListTenantsPage page = new ListTenantsPage.PageFactory(source).create();
+ int iterations = 0;
+ for (Tenant tenant : page.iterateAll()) {
+ assertEquals("tenant" + iterations, tenant.getTenantId());
+ iterations++;
+ if (iterations == 3) {
+ assertEquals(1, source.calls.size());
+ assertNull(source.calls.get(0));
+ response = new ListTenantsResponse(
+ ImmutableList.of(newTenant("tenant3"), newTenant("tenant4"), newTenant("tenant5")),
+ ListTenantsPage.END_OF_LIST);
+ source.response = response;
+ }
+ }
+
+ assertEquals(6, iterations);
+ assertEquals(2, source.calls.size());
+ assertEquals("token", source.calls.get(1));
+ }
+
+ @Test
+ public void testListTenantsPagedIterator() throws FirebaseAuthException, IOException {
+ ListTenantsResponse response = new ListTenantsResponse(
+ ImmutableList.of(newTenant("tenant0"), newTenant("tenant1"), newTenant("tenant2")),
+ "token");
+ TestTenantSource source = new TestTenantSource(response);
+ ListTenantsPage page = new ListTenantsPage.PageFactory(source).create();
+ Iterator tenants = page.iterateAll().iterator();
+ int iterations = 0;
+ while (tenants.hasNext()) {
+ assertEquals("tenant" + iterations, tenants.next().getTenantId());
+ iterations++;
+ if (iterations == 3) {
+ assertEquals(1, source.calls.size());
+ assertNull(source.calls.get(0));
+ response = new ListTenantsResponse(
+ ImmutableList.of(newTenant("tenant3"), newTenant("tenant4"), newTenant("tenant5")),
+ ListTenantsPage.END_OF_LIST);
+ source.response = response;
+ }
+ }
+
+ assertEquals(6, iterations);
+ assertEquals(2, source.calls.size());
+ assertEquals("token", source.calls.get(1));
+ assertFalse(tenants.hasNext());
+ try {
+ tenants.next();
+ } catch (NoSuchElementException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testPageWithNoTenants() throws FirebaseAuthException {
+ ListTenantsResponse response = new ListTenantsResponse(
+ ImmutableList.of(),
+ ListTenantsPage.END_OF_LIST);
+ TestTenantSource source = new TestTenantSource(response);
+ ListTenantsPage page = new ListTenantsPage.PageFactory(source).create();
+ assertFalse(page.hasNextPage());
+ assertEquals(ListTenantsPage.END_OF_LIST, page.getNextPageToken());
+ assertNull(page.getNextPage());
+ assertEquals(0, ImmutableList.copyOf(page.getValues()).size());
+ assertEquals(1, source.calls.size());
+ }
+
+ @Test
+ public void testIterableWithNoTenants() throws FirebaseAuthException {
+ ListTenantsResponse response = new ListTenantsResponse(
+ ImmutableList.of(),
+ ListTenantsPage.END_OF_LIST);
+ TestTenantSource source = new TestTenantSource(response);
+ ListTenantsPage page = new ListTenantsPage.PageFactory(source).create();
+ for (Tenant tenant : page.iterateAll()) {
+ fail("Should not be able to iterate, but got: " + tenant);
+ }
+ assertEquals(1, source.calls.size());
+ }
+
+ @Test
+ public void testIteratorWithNoTenants() throws FirebaseAuthException {
+ ListTenantsResponse response = new ListTenantsResponse(
+ ImmutableList.of(),
+ ListTenantsPage.END_OF_LIST);
+ TestTenantSource source = new TestTenantSource(response);
+
+ ListTenantsPage page = new ListTenantsPage.PageFactory(source).create();
+ Iterator iterator = page.iterateAll().iterator();
+ while (iterator.hasNext()) {
+ fail("Should not be able to iterate");
+ }
+ assertEquals(1, source.calls.size());
+ }
+
+ @Test
+ public void testRemove() throws FirebaseAuthException, IOException {
+ ListTenantsResponse response = new ListTenantsResponse(
+ ImmutableList.of(newTenant("tenant1")),
+ ListTenantsPage.END_OF_LIST);
+ TestTenantSource source = new TestTenantSource(response);
+
+ ListTenantsPage page = new ListTenantsPage.PageFactory(source).create();
+ Iterator iterator = page.iterateAll().iterator();
+ while (iterator.hasNext()) {
+ assertNotNull(iterator.next());
+ try {
+ iterator.remove();
+ } catch (UnsupportedOperationException expected) {
+ // expected
+ }
+ }
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testNullSource() {
+ new ListTenantsPage.PageFactory(null);
+ }
+
+ @Test
+ public void testInvalidPageToken() throws IOException {
+ TestTenantSource source = new TestTenantSource(1);
+ try {
+ new ListTenantsPage.PageFactory(source, 1000, "");
+ fail("No error thrown for empty page token");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testInvalidMaxResults() throws IOException {
+ TestTenantSource source = new TestTenantSource(1);
+ try {
+ new ListTenantsPage.PageFactory(source, 1001, "");
+ fail("No error thrown for maxResult > 1000");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+
+ try {
+ new ListTenantsPage.PageFactory(source, 0, "next");
+ fail("No error thrown for maxResult = 0");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+
+ try {
+ new ListTenantsPage.PageFactory(source, -1, "next");
+ fail("No error thrown for maxResult < 0");
+ } catch (IllegalArgumentException expected) {
+ // expected
+ }
+ }
+
+ private static Tenant newTenant(String tenantId) throws IOException {
+ return Utils.getDefaultJsonFactory().fromString(
+ String.format("{\"tenantId\":\"%s\"}", tenantId), Tenant.class);
+ }
+
+ private static class TestTenantSource implements ListTenantsPage.TenantSource {
+
+ private ListTenantsResponse response;
+ private List calls = new ArrayList<>();
+
+ TestTenantSource(int tenantCount) throws IOException {
+ ImmutableList.Builder tenants = ImmutableList.builder();
+ for (int i = 0; i < tenantCount; i++) {
+ tenants.add(newTenant("tenant" + i));
+ }
+ this.response = new ListTenantsResponse(tenants.build(), ListTenantsPage.END_OF_LIST);
+ }
+
+ TestTenantSource(ListTenantsResponse response) {
+ this.response = response;
+ }
+
+ @Override
+ public ListTenantsResponse fetch(int maxResults, String pageToken) {
+ calls.add(pageToken);
+ return response;
+ }
+ }
+}