diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 0647084c8..e84cbd008 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -95,7 +95,7 @@ private FirebaseUserManager(Builder builder) { this.userMgtBaseUrl = idToolkitUrlV1; this.idpConfigMgtBaseUrl = idToolkitUrlV2; } else { - checkArgument(!tenantId.isEmpty(), "Tenant ID must not be empty."); + Utils.checkTenantId(tenantId); this.userMgtBaseUrl = idToolkitUrlV1 + "/tenants/" + tenantId; this.idpConfigMgtBaseUrl = idToolkitUrlV2 + "/tenants/" + tenantId; } @@ -335,12 +335,12 @@ void deleteSamlProviderConfig(String providerId) throws FirebaseAuthException { } private static String getOidcUrlSuffix(String providerId) { - checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + OidcProviderConfig.checkOidcProviderId(providerId); return "/oauthIdpConfigs/" + providerId; } private static String getSamlUrlSuffix(String providerId) { - checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + SamlProviderConfig.checkSamlProviderId(providerId); return "/inboundSamlConfigs/" + providerId; } diff --git a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java index 0a4f7c0b8..43caa411c 100644 --- a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -21,6 +21,7 @@ import com.google.api.client.json.GenericJson; import com.google.api.client.util.Key; import com.google.common.base.Strings; +import com.google.firebase.auth.internal.Utils; import java.util.HashMap; import java.util.Map; @@ -77,6 +78,7 @@ static void checkOidcProviderId(String providerId) { checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); checkArgument(providerId.startsWith("oidc."), "Invalid OIDC provider ID (must be prefixed with 'oidc.'): " + providerId); + checkArgument(Utils.isSafeUrlSegment(providerId), "Invalid OIDC provider ID: " + providerId); } static Map ensureResponseType(Map properties) { diff --git a/src/main/java/com/google/firebase/auth/SamlProviderConfig.java b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java index 76f8594a8..7668ace3e 100644 --- a/src/main/java/com/google/firebase/auth/SamlProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java @@ -24,6 +24,7 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.internal.Utils; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -84,6 +85,7 @@ static void checkSamlProviderId(String providerId) { checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); checkArgument(providerId.startsWith("saml."), "Invalid SAML provider ID (must be prefixed with 'saml.'): " + providerId); + checkArgument(Utils.isSafeUrlSegment(providerId), "Invalid SAML provider ID: " + providerId); } private static List ensureNestedList(Map outerMap, String id) { diff --git a/src/main/java/com/google/firebase/auth/internal/Utils.java b/src/main/java/com/google/firebase/auth/internal/Utils.java index 244a844f9..1b267f175 100644 --- a/src/main/java/com/google/firebase/auth/internal/Utils.java +++ b/src/main/java/com/google/firebase/auth/internal/Utils.java @@ -16,14 +16,20 @@ package com.google.firebase.auth.internal; +import static com.google.common.base.Preconditions.checkArgument; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.firebase.internal.FirebaseProcessEnvironment; +import java.util.regex.Pattern; public class Utils { @VisibleForTesting public static final String AUTH_EMULATOR_HOST = "FIREBASE_AUTH_EMULATOR_HOST"; + private static final Pattern SAFE_URL_SEGMENT_PATTERN = + Pattern.compile("^[a-zA-Z0-9\\-_.~]+$"); + public static boolean isEmulatorMode() { return !Strings.isNullOrEmpty(getEmulatorHost()); } @@ -32,4 +38,15 @@ public static String getEmulatorHost() { return FirebaseProcessEnvironment.getenv(AUTH_EMULATOR_HOST); } + public static boolean isSafeUrlSegment(String segment) { + return !Strings.isNullOrEmpty(segment) + && !segment.endsWith(".") + && SAFE_URL_SEGMENT_PATTERN.matcher(segment).matches(); + } + + public static void checkTenantId(String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + checkArgument(isSafeUrlSegment(tenantId), "Invalid tenant ID: " + tenantId); + } + } diff --git a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java index 15fb6d618..e9bce889f 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java @@ -118,7 +118,7 @@ ListTenantsResponse listTenants(int maxResults, String pageToken) } private static String getTenantUrlSuffix(String tenantId) { - checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + Utils.checkTenantId(tenantId); return "/tenants/" + tenantId; } } diff --git a/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java index 57d215e96..98ecc0810 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java @@ -21,6 +21,7 @@ import com.google.api.client.util.Key; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.internal.Utils; import java.util.HashMap; import java.util.Map; @@ -147,7 +148,7 @@ public static final class UpdateRequest { * @throws IllegalArgumentException If the tenant ID is null or empty. */ public UpdateRequest(String tenantId) { - checkArgument(!Strings.isNullOrEmpty(tenantId), "tenant ID must not be null or empty"); + Utils.checkTenantId(tenantId); this.tenantId = tenantId; } diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java index bc374c036..26139e748 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java @@ -16,18 +16,16 @@ package com.google.firebase.auth.multitenancy; -import static com.google.common.base.Preconditions.checkArgument; - import com.google.api.core.ApiAsyncFunction; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; -import com.google.common.base.Strings; import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.FirebaseApp; import com.google.firebase.auth.AbstractFirebaseAuth; import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.FirebaseToken; import com.google.firebase.auth.SessionCookieOptions; +import com.google.firebase.auth.internal.Utils; /** * The tenant-aware Firebase client. @@ -41,7 +39,7 @@ public final class TenantAwareFirebaseAuth extends AbstractFirebaseAuth { private TenantAwareFirebaseAuth(Builder builder) { super(builder); - checkArgument(!Strings.isNullOrEmpty(builder.tenantId)); + Utils.checkTenantId(builder.tenantId); this.tenantId = builder.tenantId; } diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java index a30c0b884..96e642010 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java @@ -22,10 +22,10 @@ import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.core.ApiFuture; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Strings; import com.google.firebase.FirebaseApp; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.internal.Utils; import com.google.firebase.auth.multitenancy.ListTenantsPage.DefaultTenantSource; import com.google.firebase.auth.multitenancy.ListTenantsPage.PageFactory; import com.google.firebase.auth.multitenancy.ListTenantsPage.TenantSource; @@ -82,7 +82,7 @@ public Tenant getTenant(@NonNull String tenantId) throws FirebaseAuthException { } public synchronized TenantAwareFirebaseAuth getAuthForTenant(@NonNull String tenantId) { - checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + Utils.checkTenantId(tenantId); if (!tenantAwareAuths.containsKey(tenantId)) { tenantAwareAuths.put(tenantId, TenantAwareFirebaseAuth.fromApp(firebaseApp, tenantId)); } @@ -103,7 +103,7 @@ public ApiFuture getTenantAsync(@NonNull String tenantId) { } private CallableOperation getTenantOp(final String tenantId) { - checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + Utils.checkTenantId(tenantId); return new CallableOperation() { @Override protected Tenant execute() throws FirebaseAuthException { @@ -280,7 +280,7 @@ public ApiFuture deleteTenantAsync(String tenantId) { } private CallableOperation deleteTenantOp(final String tenantId) { - checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + Utils.checkTenantId(tenantId); return new CallableOperation() { @Override protected Void execute() throws FirebaseAuthException { diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java index 1387d2226..c1ce649bf 100644 --- a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java +++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java @@ -40,6 +40,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Map; class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppService { @@ -190,29 +191,29 @@ private CallableOperation, FirebaseProjectManagementException> listA @Override protected List execute() throws FirebaseProjectManagementException { String url = String.format( - "%s/v1beta1/projects/%s/%s?page_size=%d", + "%s/v1beta1/projects/%s/%s", FIREBASE_PROJECT_MANAGEMENT_URL, projectId, - platformResourceName, - MAXIMUM_LIST_APPS_PAGE_SIZE); + platformResourceName); + Map queryParams = + ImmutableMap.of("page_size", MAXIMUM_LIST_APPS_PAGE_SIZE); ImmutableList.Builder builder = ImmutableList.builder(); ListAppsResponse parsedResponse; do { parsedResponse = new ListAppsResponse(); - httpHelper.makeGetRequest(url, parsedResponse, projectId, "Project ID"); + httpHelper.makeGetRequest( + url, queryParams, parsedResponse, projectId, "Project ID"); if (parsedResponse.apps == null) { break; } for (AppResponse app : parsedResponse.apps) { builder.add(createAppFromAppIdFunction.apply(app.appId)); } - url = String.format( - "%s/v1beta1/projects/%s/%s?page_token=%s&page_size=%d", - FIREBASE_PROJECT_MANAGEMENT_URL, - projectId, - platformResourceName, - parsedResponse.nextPageToken, - MAXIMUM_LIST_APPS_PAGE_SIZE); + if (!Strings.isNullOrEmpty(parsedResponse.nextPageToken)) { + queryParams = ImmutableMap.of( + "page_token", parsedResponse.nextPageToken, + "page_size", MAXIMUM_LIST_APPS_PAGE_SIZE); + } } while (!Strings.isNullOrEmpty(parsedResponse.nextPageToken)); return builder.build(); } diff --git a/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java b/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java index d0f5942b1..70e97df38 100644 --- a/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java +++ b/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java @@ -26,6 +26,7 @@ import com.google.firebase.internal.ErrorHandlingHttpClient; import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.SdkUtils; +import java.util.Map; final class HttpHelper { @@ -56,6 +57,19 @@ IncomingHttpResponse makeGetRequest( requestIdentifierDescription); } + IncomingHttpResponse makeGetRequest( + String url, + Map queryParams, + T parsedResponseInstance, + String requestIdentifier, + String requestIdentifierDescription) throws FirebaseProjectManagementException { + return makeRequest( + HttpRequestInfo.buildGetRequest(url).addAllParameters(queryParams), + parsedResponseInstance, + requestIdentifier, + requestIdentifierDescription); + } + IncomingHttpResponse makePostRequest( String url, Object payload, diff --git a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java index 0fd6299ee..43bbc275c 100644 --- a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java +++ b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java @@ -20,12 +20,15 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; import com.google.firebase.internal.ApiClientUtils; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.Test; @@ -123,6 +126,24 @@ public void testCreateRequestInvalidProviderId() { new OidcProviderConfig.CreateRequest().setProviderId("saml.provider-id"); } + @Test + public void testCreateRequestInvalidProviderIdCharacters() { + List invalidIds = ImmutableList.of( + "oidc.", + "oidc.provider.", + "oidc.provider/sub", + "oidc.provider?param=1", + "oidc.provider#frag"); + for (String id : invalidIds) { + try { + new OidcProviderConfig.CreateRequest().setProviderId(id); + fail("No error thrown for invalid provider ID: " + id); + } catch (IllegalArgumentException expected) { + assertEquals("Invalid OIDC provider ID: " + id, expected.getMessage()); + } + } + } + @Test(expected = IllegalArgumentException.class) public void testCreateRequestMissingDisplayName() { new OidcProviderConfig.CreateRequest().setDisplayName(null); @@ -200,6 +221,24 @@ public void testUpdateRequestInvalidProviderId() { new OidcProviderConfig.UpdateRequest("saml.provider-id"); } + @Test + public void testUpdateRequestInvalidProviderIdCharacters() { + List invalidIds = ImmutableList.of( + "oidc.", + "oidc.provider.", + "oidc.provider/sub", + "oidc.provider?param=1", + "oidc.provider#frag"); + for (String id : invalidIds) { + try { + new OidcProviderConfig.UpdateRequest(id); + fail("No error thrown for invalid provider ID: " + id); + } catch (IllegalArgumentException expected) { + assertEquals("Invalid OIDC provider ID: " + id, expected.getMessage()); + } + } + } + @Test(expected = IllegalArgumentException.class) public void testUpdateRequestMissingDisplayName() { new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName(null); diff --git a/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java b/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java index b9464452e..a21d09d39 100644 --- a/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java +++ b/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import com.google.api.client.json.JsonFactory; import com.google.common.collect.ImmutableList; @@ -139,6 +140,24 @@ public void testCreateRequestInvalidProviderId() { new SamlProviderConfig.CreateRequest().setProviderId("oidc.provider-id"); } + @Test + public void testCreateRequestInvalidProviderIdCharacters() { + List invalidIds = ImmutableList.of( + "saml.", + "saml.provider.", + "saml.provider/sub", + "saml.provider?param=1", + "saml.provider#frag"); + for (String id : invalidIds) { + try { + new SamlProviderConfig.CreateRequest().setProviderId(id); + fail("No error thrown for invalid provider ID: " + id); + } catch (IllegalArgumentException expected) { + assertEquals("Invalid SAML provider ID: " + id, expected.getMessage()); + } + } + } + @Test(expected = IllegalArgumentException.class) public void testCreateRequestMissingDisplayName() { new SamlProviderConfig.CreateRequest().setDisplayName(null); @@ -270,6 +289,24 @@ public void testUpdateRequestInvalidProviderId() { new SamlProviderConfig.UpdateRequest("oidc.invalid-saml-provider-id"); } + @Test + public void testUpdateRequestInvalidProviderIdCharacters() { + List invalidIds = ImmutableList.of( + "saml.", + "saml.provider.", + "saml.provider/sub", + "saml.provider?param=1", + "saml.provider#frag"); + for (String id : invalidIds) { + try { + new SamlProviderConfig.UpdateRequest(id); + fail("No error thrown for invalid provider ID: " + id); + } catch (IllegalArgumentException expected) { + assertEquals("Invalid SAML provider ID: " + id, expected.getMessage()); + } + } + } + @Test(expected = IllegalArgumentException.class) public void testUpdateRequestMissingDisplayName() { new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName(null); diff --git a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java index cdb5852d0..0a2311579 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java @@ -99,6 +99,41 @@ public void testGetTenant() throws Exception { checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_1"); } + @Test + public void testTenantOperationsInvalidTenantId() throws Exception { + initializeAppForTenantManagement("{}"); + TenantManager tenantManager = FirebaseAuth.getInstance().getTenantManager(); + List invalidIds = ImmutableList.of( + ".", + "..", + "tenant.", + "tenant/sub", + "tenant?param=1", + "tenant#frag"); + for (String id : invalidIds) { + try { + tenantManager.getAuthForTenant(id); + fail("No error thrown for getAuthForTenant with invalid tenant ID: " + id); + } catch (IllegalArgumentException expected) { + assertEquals("Invalid tenant ID: " + id, expected.getMessage()); + } + + try { + tenantManager.getTenant(id); + fail("No error thrown for getTenant with invalid tenant ID: " + id); + } catch (IllegalArgumentException expected) { + assertEquals("Invalid tenant ID: " + id, expected.getMessage()); + } + + try { + tenantManager.deleteTenant(id); + fail("No error thrown for deleteTenant with invalid tenant ID: " + id); + } catch (IllegalArgumentException expected) { + assertEquals("Invalid tenant ID: " + id, expected.getMessage()); + } + } + } + @Test public void testGetTenantWithNotFoundError() { TestResponseInterceptor interceptor = diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java index f4ee0a326..21e8b8984 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java @@ -19,11 +19,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; import com.google.firebase.internal.ApiClientUtils; import java.io.IOException; +import java.util.List; import java.util.Map; import org.junit.Test; @@ -75,6 +78,25 @@ public void testUpdateRequestFromTenantId() throws IOException { assertTrue((boolean) properties.get("enableEmailLinkSignin")); } + @Test + public void testUpdateRequestInvalidTenantId() { + List invalidIds = ImmutableList.of( + ".", + "..", + "tenant.", + "tenant/sub", + "tenant?param=1", + "tenant#frag"); + for (String id : invalidIds) { + try { + new Tenant.UpdateRequest(id); + fail("No error thrown for invalid tenant ID: " + id); + } catch (IllegalArgumentException expected) { + assertEquals("Invalid tenant ID: " + id, expected.getMessage()); + } + } + } + @Test public void testCreateRequest() throws IOException { Tenant.CreateRequest createRequest = new Tenant.CreateRequest(); diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java index 971b80369..f917f586f 100644 --- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java @@ -455,6 +455,39 @@ public void listIosAppsAsyncMultiplePages() throws Exception { assertEquals(iosAppList.get(1).getAppId(), "test-ios-app-id-2"); } + @Test + public void listIosAppsMultiplePagesEncodesPageToken() throws Exception { + firstRpcResponse.setContent( + "{\"apps\": [" + + "{\"name\" : \"projects/test-project-id/iosApps/test-ios-app-id-1\", " + + "\"appId\" : \"test-ios-app-id-1\", " + + "\"displayName\" : \"display-name\", " + + "\"projectId\" : \"test-project-id\", " + + "\"bundleId\" : \"test.ios.app1\"}], " + + "\"nextPageToken\" : \"token+1=2&b=3#frag\"}"); + MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); + secondRpcResponse.setContent(LIST_IOS_APPS_PAGE_2_RESPONSE); + serviceImpl = initServiceImpl( + ImmutableList.of(firstRpcResponse, secondRpcResponse), + interceptor); + + List iosAppList = serviceImpl.listIosApps(PROJECT_ID); + + String firstRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/iosApps?page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + String secondRpcExpectedUrl = String.format( + "%s/v1beta1/projects/%s/iosApps?page_token=token%%2B1%%3D2%%26b%%3D3%%23frag&page_size=%d", + FIREBASE_PROJECT_MANAGEMENT_URL, + PROJECT_ID, + MAXIMUM_LIST_APPS_PAGE_SIZE); + checkRequestHeader(0, firstRpcExpectedUrl, HttpMethod.GET); + checkRequestHeader(1, secondRpcExpectedUrl, HttpMethod.GET); + assertEquals(iosAppList.size(), 2); + } + @Test public void createIosApp() throws Exception { firstRpcResponse.setContent(CREATE_IOS_RESPONSE);