Repository: firebase/firebase-admin-java
Pull request #385 (2020-03-25 22:07:13)

Add integration tests for TenantManager operations.
This pull request adds some integration testing for all of the tenant operations in TenantManager. Several bugs were uncovered after running the tests, so these have been fixed. This is part of the initiative to adding multi-tenancy support (see issue #332).


Patch:
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index ec1cbe6..23867e3 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -262,15 +262,15 @@ class FirebaseUserManager {
 
   ListTenantsResponse listTenants(int maxResults, String pageToken)
       throws FirebaseAuthException {
-    ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder()
-        .put("pageSize", maxResults);
+    ImmutableMap.Builder<String, Object> builder =
+        ImmutableMap.<String, Object>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");
+    GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants");
     url.putAll(builder.build());
     ListTenantsResponse response = sendRequest("GET", url, null, ListTenantsResponse.class);
     if (response == null) {
@@ -329,9 +329,13 @@ class FirebaseUserManager {
     HttpResponse response = null;
     try {
       HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null;
-      HttpRequest request = requestFactory.buildRequest(method, url, httpContent);
+      HttpRequest request =
+          requestFactory.buildRequest(method.equals("PATCH") ? "POST" : method, url, httpContent);
       request.setParser(new JsonObjectParser(jsonFactory));
       request.getHeaders().set(CLIENT_VERSION_HEADER, clientVersion);
+      if (method.equals("PATCH")) {
+        request.getHeaders().set("X-HTTP-Method-Override", "PATCH");
+      }
       request.setResponseInterceptor(interceptor);
       response = request.execute();
       return response.parseAs(clazz);
diff --git a/src/main/java/com/google/firebase/auth/Tenant.java b/src/main/java/com/google/firebase/auth/Tenant.java
index 6e48972..6641a04 100644
--- a/src/main/java/com/google/firebase/auth/Tenant.java
+++ b/src/main/java/com/google/firebase/auth/Tenant.java
@@ -31,9 +31,12 @@ import java.util.Map;
  */
 public final class Tenant {
 
-  @Key("name")
+  // Lazily initialized from 'resourceName'.
   private String tenantId;
 
+  @Key("name")
+  private String resourceName;
+
   @Key("displayName")
   private String displayName;
 
@@ -44,6 +47,9 @@ public final class Tenant {
   private boolean emailLinkSignInEnabled;
 
   public String getTenantId() {
+    if (tenantId == null) {
+      tenantId = resourceName.substring(resourceName.lastIndexOf("/") + 1);
+    }
     return tenantId;
   }
 
diff --git a/src/main/java/com/google/firebase/auth/TenantManager.java b/src/main/java/com/google/firebase/auth/TenantManager.java
index 54abbe8..c4e54d4 100644
--- a/src/main/java/com/google/firebase/auth/TenantManager.java
+++ b/src/main/java/com/google/firebase/auth/TenantManager.java
@@ -178,6 +178,19 @@ public final class TenantManager {
     return createTenantOp(request).callAsync(firebaseApp);
   }
 
+  private CallableOperation<Tenant, FirebaseAuthException> createTenantOp(
+      final CreateRequest request) {
+    // TODO(micahstairs): Add a check to make sure the app has not been destroyed yet.
+    checkNotNull(request, "create request must not be null");
+    return new CallableOperation<Tenant, FirebaseAuthException>() {
+      @Override
+      protected Tenant execute() throws FirebaseAuthException {
+        return userManager.createTenant(request);
+      }
+    };
+  }
+
+
   /**
    * Updates an existing user account with the attributes contained in the specified {@link
    * UpdateRequest}.
@@ -215,18 +228,6 @@ public final class TenantManager {
     };
   }
 
-  private CallableOperation<Tenant, FirebaseAuthException> createTenantOp(
-      final CreateRequest request) {
-    // TODO(micahstairs): Add a check to make sure the app has not been destroyed yet.
-    checkNotNull(request, "create request must not be null");
-    return new CallableOperation<Tenant, FirebaseAuthException>() {
-      @Override
-      protected Tenant execute() throws FirebaseAuthException {
-        return userManager.createTenant(request);
-      }
-    };
-  }
-
   /**
    * Deletes the tenant identified by the specified tenant ID.
    *


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
index 6a8361d..d6a41d4 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -46,8 +46,6 @@ import com.google.common.util.concurrent.MoreExecutors;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.ImplFirebaseTrampolines;
-import com.google.firebase.auth.UserRecord.CreateRequest;
-import com.google.firebase.auth.UserRecord.UpdateRequest;
 import com.google.firebase.auth.hash.Scrypt;
 import com.google.firebase.testing.IntegrationTestUtils;
 import java.io.IOException;
@@ -116,7 +114,7 @@ public class FirebaseAuthIT {
   @Test
   public void testUpdateNonExistingUser() throws Exception {
     try {
-      auth.updateUserAsync(new UpdateRequest("non.existing")).get();
+      auth.updateUserAsync(new UserRecord.UpdateRequest("non.existing")).get();
       fail("No error thrown for non existing uid");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
@@ -141,7 +139,7 @@ public class FirebaseAuthIT {
   public void testCreateUserWithParams() throws Exception {
     RandomUser randomUser = RandomUser.create();
     String phone = randomPhoneNumber();
-    CreateRequest user = new CreateRequest()
+    UserRecord.CreateRequest user = new UserRecord.CreateRequest()
         .setUid(randomUser.uid)
         .setEmail(randomUser.email)
         .setPhoneNumber(phone)
@@ -168,7 +166,7 @@ public class FirebaseAuthIT {
       assertTrue(providers.contains("password"));
       assertTrue(providers.contains("phone"));
 
-      checkRecreate(randomUser.uid);
+      checkRecreateUser(randomUser.uid);
     } finally {
       auth.deleteUserAsync(userRecord.getUid()).get();
     }
@@ -177,7 +175,7 @@ public class FirebaseAuthIT {
   @Test
   public void testUserLifecycle() throws Exception {
     // Create user
-    UserRecord userRecord = auth.createUserAsync(new CreateRequest()).get();
+    UserRecord userRecord = auth.createUserAsync(new UserRecord.CreateRequest()).get();
     String uid = userRecord.getUid();
 
     // Get user
@@ -197,7 +195,7 @@ public class FirebaseAuthIT {
     // Update user
     RandomUser randomUser = RandomUser.create();
     String phone = randomPhoneNumber();
-    UpdateRequest request = userRecord.updateRequest()
+    UserRecord.UpdateRequest request = userRecord.updateRequest()
         .setDisplayName("Updated Name")
         .setEmail(randomUser.email)
         .setPhoneNumber(phone)
@@ -240,7 +238,7 @@ public class FirebaseAuthIT {
     auth.deleteUserAsync(userRecord.getUid()).get();
     try {
       auth.getUserAsync(userRecord.getUid()).get();
-      fail("No error thrown for deleted user");
+      fail("No error thrown for getting a deleted user");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
       assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
@@ -253,9 +251,11 @@ public class FirebaseAuthIT {
     final List<String> uids = new ArrayList<>();
 
     try {
-      uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid());
-      uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid());
-      uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid());
+      for (int i = 0; i < 3; i++) {
+        UserRecord.CreateRequest createRequest =
+            new UserRecord.CreateRequest().setPassword("password");
+        uids.add(auth.createUserAsync(createRequest).get().getUid());
+      }
 
       // Test list by batches
       final AtomicInteger collected = new AtomicInteger(0);
@@ -320,9 +320,121 @@ public class FirebaseAuthIT {
     }
   }
 
+  @Test
+  public void testTenantLifecycle() throws Exception {
+    TenantManager tenantManager = auth.getTenantManager();
+
+    // Create tenant
+    Tenant.CreateRequest createRequest = new Tenant.CreateRequest().setDisplayName("DisplayName");
+    Tenant tenant = tenantManager.createTenantAsync(createRequest).get();
+    assertEquals("DisplayName", tenant.getDisplayName());
+    assertFalse(tenant.isPasswordSignInAllowed());
+    assertFalse(tenant.isEmailLinkSignInEnabled());
+    String tenantId = tenant.getTenantId();
+
+    // Get tenant
+    tenant = tenantManager.getTenantAsync(tenantId).get();
+    assertEquals(tenantId, tenant.getTenantId());
+    assertEquals("DisplayName", tenant.getDisplayName());
+    assertFalse(tenant.isPasswordSignInAllowed());
+    assertFalse(tenant.isEmailLinkSignInEnabled());
+
+    // Update tenant
+    Tenant.UpdateRequest updateRequest = tenant.updateRequest()
+        .setDisplayName("UpdatedName")
+        .setPasswordSignInAllowed(true)
+        .setEmailLinkSignInEnabled(true);
+    tenant = tenantManager.updateTenantAsync(updateRequest).get();
+    assertEquals(tenantId, tenant.getTenantId());
+    assertEquals("UpdatedName", tenant.getDisplayName());
+    assertTrue(tenant.isPasswordSignInAllowed());
+    assertTrue(tenant.isEmailLinkSignInEnabled());
+
+    // Delete tenant
+    tenantManager.deleteTenantAsync(tenant.getTenantId()).get();
+    try {
+      tenantManager.getTenantAsync(tenant.getTenantId()).get();
+      fail("No error thrown for getting a deleted tenant");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertEquals(FirebaseUserManager.TENANT_NOT_FOUND_ERROR,
+          ((FirebaseAuthException) e.getCause()).getErrorCode());
+    }
+  }
+
+  @Test
+  public void testListTenants() throws Exception {
+    TenantManager tenantManager = auth.getTenantManager();
+    final List<String> tenantIds = new ArrayList<>();
+
+    try {
+      for (int i = 0; i < 3; i++) {
+        Tenant.CreateRequest createRequest =
+            new Tenant.CreateRequest().setDisplayName("DisplayName" + i);
+        tenantIds.add(tenantManager.createTenantAsync(createRequest).get().getTenantId());
+      }
+
+      // Test list by batches
+      final AtomicInteger collected = new AtomicInteger(0);
+      ListTenantsPage page = tenantManager.listTenantsAsync(null).get();
+      while (page != null) {
+        for (Tenant tenant : page.getValues()) {
+          if (tenantIds.contains(tenant.getTenantId())) {
+            collected.incrementAndGet();
+            assertNotNull(tenant.getDisplayName());
+          }
+        }
+        page = page.getNextPage();
+      }
+      assertEquals(tenantIds.size(), collected.get());
+
+      // Test iterate all
+      collected.set(0);
+      page = tenantManager.listTenantsAsync(null).get();
+      for (Tenant tenant : page.iterateAll()) {
+        if (tenantIds.contains(tenant.getTenantId())) {
+          collected.incrementAndGet();
+          assertNotNull(tenant.getDisplayName());
+        }
+      }
+      assertEquals(tenantIds.size(), collected.get());
+
+      // Test iterate async
+      collected.set(0);
+      final Semaphore semaphore = new Semaphore(0);
+      final AtomicReference<Throwable> error = new AtomicReference<>();
+      ApiFuture<ListTenantsPage> pageFuture = tenantManager.listTenantsAsync(null);
+      ApiFutures.addCallback(pageFuture, new ApiFutureCallback<ListTenantsPage>() {
+        @Override
+        public void onFailure(Throwable t) {
+          error.set(t);
+          semaphore.release();
+        }
+
+        @Override
+        public void onSuccess(ListTenantsPage result) {
+          for (Tenant tenant : result.iterateAll()) {
+            if (tenantIds.contains(tenant.getTenantId())) {
+              collected.incrementAndGet();
+              assertNotNull(tenant.getDisplayName());
+            }
+          }
+          semaphore.release();
+        }
+      }, MoreExecutors.directExecutor());
+      semaphore.acquire();
+      assertEquals(tenantIds.size(), collected.get());
+      assertNull(error.get());
+    } finally {
+      for (String tenantId : tenantIds) {
+        tenantManager.deleteTenantAsync(tenantId).get();
+      }
+    }
+  }
+
   @Test
   public void testCustomClaims() throws Exception {
-    UserRecord userRecord = auth.createUserAsync(new CreateRequest()).get();
+    UserRecord userRecord = auth.createUserAsync(new UserRecord.CreateRequest()).get();
     String uid = userRecord.getUid();
 
     try {
@@ -413,7 +525,7 @@ public class FirebaseAuthIT {
     }
     idToken = signInWithCustomToken(customToken);
     decoded = auth.verifyIdTokenAsync(idToken, true).get();
-    assertEquals("user2", decoded.getUid());    
+    assertEquals("user2", decoded.getUid());
     auth.deleteUserAsync("user2");
   }
 
@@ -524,7 +636,7 @@ public class FirebaseAuthIT {
   @Test
   public void testGeneratePasswordResetLink() throws Exception {
     RandomUser user = RandomUser.create();
-    auth.createUser(new CreateRequest()
+    auth.createUser(new UserRecord.CreateRequest()
         .setUid(user.uid)
         .setEmail(user.email)
         .setEmailVerified(false)
@@ -549,7 +661,7 @@ public class FirebaseAuthIT {
   @Test
   public void testGenerateEmailVerificationResetLink() throws Exception {
     RandomUser user = RandomUser.create();
-    auth.createUser(new CreateRequest()
+    auth.createUser(new UserRecord.CreateRequest()
         .setUid(user.uid)
         .setEmail(user.email)
         .setEmailVerified(false)
@@ -572,7 +684,7 @@ public class FirebaseAuthIT {
   @Test
   public void testGenerateSignInWithEmailLink() throws Exception {
     RandomUser user = RandomUser.create();
-    auth.createUser(new CreateRequest()
+    auth.createUser(new UserRecord.CreateRequest()
         .setUid(user.uid)
         .setEmail(user.email)
         .setEmailVerified(false)
@@ -686,9 +798,9 @@ public class FirebaseAuthIT {
     }
   }
 
-  private void checkRecreate(String uid) throws Exception {
+  private void checkRecreateUser(String uid) throws Exception {
     try {
-      auth.createUserAsync(new CreateRequest().setUid(uid)).get();
+      auth.createUserAsync(new UserRecord.CreateRequest().setUid(uid)).get();
       fail("No error thrown for creating user with existing ID");
     } catch (ExecutionException e) {
       assertTrue(e.getCause() instanceof FirebaseAuthException);
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index 47227b3..a9b8881 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -458,7 +458,7 @@ public class FirebaseUserManagerTest {
     checkTenant(tenants.get(1), "TENANT_2");
     assertEquals("", page.getNextPageToken());
     checkRequestHeaders(interceptor);
-    checkUrl(interceptor, "GET", TENANTS_BASE_URL + ":list");
+    checkUrl(interceptor, "GET", TENANTS_BASE_URL);
     GenericUrl url = interceptor.getResponse().getRequest().getUrl();
     assertEquals(999, url.getFirst("pageSize"));
     assertNull(url.getFirst("pageToken"));
@@ -477,7 +477,7 @@ public class FirebaseUserManagerTest {
     checkTenant(tenants.get(1), "TENANT_2");
     assertEquals("", page.getNextPageToken());
     checkRequestHeaders(interceptor);
-    checkUrl(interceptor, "GET", TENANTS_BASE_URL + ":list");
+    checkUrl(interceptor, "GET", TENANTS_BASE_URL);
     GenericUrl url = interceptor.getResponse().getRequest().getUrl();
     assertEquals(999, url.getFirst("pageSize"));
     assertEquals("token", url.getFirst("pageToken"));
@@ -1469,7 +1469,13 @@ public class FirebaseUserManagerTest {
 
   private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) {
     HttpRequest request = interceptor.getResponse().getRequest();
-    assertEquals(method, request.getRequestMethod());
+    if (method.equals("PATCH")) {
+      assertEquals("PATCH",
+          request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override"));
+      assertEquals("POST", request.getRequestMethod());
+    } else {
+      assertEquals(method, request.getRequestMethod());
+    }
     assertEquals(url, request.getUrl().toString().split("\\?")[0]);
   }
 
diff --git a/src/test/java/com/google/firebase/auth/TenantTest.java b/src/test/java/com/google/firebase/auth/TenantTest.java
index 1fc796d..a0d2719 100644
--- a/src/test/java/com/google/firebase/auth/TenantTest.java
+++ b/src/test/java/com/google/firebase/auth/TenantTest.java
@@ -34,7 +34,7 @@ public class TenantTest {
 
   private static final String TENANT_JSON_STRING = 
       "{"
-        + "\"name\":\"TENANT_ID\","
+        + "\"name\":\"projects/project-id/resource/TENANT_ID\","
         + "\"displayName\":\"DISPLAY_NAME\","
         + "\"allowPasswordSignup\":true,"
         + "\"enableEmailLinkSignin\":false"