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; + } + } +}