Repository: firebase/firebase-admin-java
Pull request #409 (2020-05-11 23:02:00)

fix(auth): Ensuring test user account cleanup with a Rule
Using a JUnit `@Rule` to ensure that temporary user accounts are cleaned up in integration tests. This helps us avoid large try-finally blocks in tests.

I also moved couple of new tenant management tests to a new `TenantManagerIT` class.


Patch:
diff --git a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java
index 689298b..e2587da 100644
--- a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java
+++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java
@@ -20,13 +20,8 @@ import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.api.client.util.Key;
 import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableMap;
 import com.google.firebase.auth.ProviderConfig.AbstractCreateRequest;
 import com.google.firebase.auth.ProviderConfig.AbstractUpdateRequest;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.HashMap;
-import java.util.Map;
 
 /**
  * Contains metadata associated with an OIDC Auth provider.
@@ -59,14 +54,6 @@ public final class OidcProviderConfig extends ProviderConfig {
     return new UpdateRequest(getProviderId());
   }
 
-  private static void assertValidUrl(String url) throws IllegalArgumentException {
-    try {
-      new URL(url);
-    } catch (MalformedURLException e) {
-      throw new IllegalArgumentException(url + " is a malformed URL", e);
-    }
-  }
-
   /**
    * A specification class for creating a new OIDC Auth provider.
    *
@@ -90,7 +77,7 @@ public final class OidcProviderConfig extends ProviderConfig {
      * @param clientId a non-null, non-empty client ID string.
      */
     public CreateRequest setClientId(String clientId) {
-      checkArgument(!Strings.isNullOrEmpty(clientId), "client ID must not be null or empty");
+      checkArgument(!Strings.isNullOrEmpty(clientId), "Client ID must not be null or empty.");
       properties.put("clientId", clientId);
       return this;
     }
@@ -98,10 +85,10 @@ public final class OidcProviderConfig extends ProviderConfig {
     /**
      * Sets the issuer for the new provider.
      *
-     * @param issuer a non-null, non-empty issuer string.
+     * @param issuer a non-null, non-empty issuer URL string.
      */
     public CreateRequest setIssuer(String issuer) {
-      checkArgument(!Strings.isNullOrEmpty(issuer), "issuer must not be null or empty");
+      checkArgument(!Strings.isNullOrEmpty(issuer), "Issuer must not be null or empty.");
       assertValidUrl(issuer);
       properties.put("issuer", issuer);
       return this;
@@ -130,10 +117,12 @@ public final class OidcProviderConfig extends ProviderConfig {
      * information persistently.
      *
      * @param tenantId a non-null, non-empty provider ID string.
-     * @throws IllegalArgumentException If the provider ID is null or empty.
+     * @throws IllegalArgumentException If the provider ID is null or empty, or if it's an invalid
+     *     format
      */
     public UpdateRequest(String providerId) {
       super(providerId);
+      checkArgument(providerId.startsWith("oidc."), "Invalid OIDC provider ID: " + providerId);
     }
 
     /**
@@ -142,7 +131,7 @@ public final class OidcProviderConfig extends ProviderConfig {
      * @param clientId a non-null, non-empty client ID string.
      */
     public UpdateRequest setClientId(String clientId) {
-      checkArgument(!Strings.isNullOrEmpty(clientId), "client ID must not be null or empty");
+      checkArgument(!Strings.isNullOrEmpty(clientId), "Client ID must not be null or empty.");
       properties.put("clientId", clientId);
       return this;
     }
@@ -150,10 +139,10 @@ public final class OidcProviderConfig extends ProviderConfig {
     /**
      * Sets the issuer for the existing provider.
      *
-     * @param issuer a non-null, non-empty issuer string.
+     * @param issuer a non-null, non-empty issuer URL string.
      */
     public UpdateRequest setIssuer(String issuer) {
-      checkArgument(!Strings.isNullOrEmpty(issuer), "issuer must not be null or empty");
+      checkArgument(!Strings.isNullOrEmpty(issuer), "Issuer must not be null or empty.");
       assertValidUrl(issuer);
       properties.put("issuer", issuer);
       return this;
diff --git a/src/main/java/com/google/firebase/auth/ProviderConfig.java b/src/main/java/com/google/firebase/auth/ProviderConfig.java
index 434daab..09ed965 100644
--- a/src/main/java/com/google/firebase/auth/ProviderConfig.java
+++ b/src/main/java/com/google/firebase/auth/ProviderConfig.java
@@ -21,6 +21,8 @@ import static com.google.common.base.Preconditions.checkArgument;
 import com.google.api.client.util.Key;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -50,6 +52,14 @@ public abstract class ProviderConfig {
     return enabled;
   }
 
+  static void assertValidUrl(String url) throws IllegalArgumentException {
+    try {
+      new URL(url);
+    } catch (MalformedURLException e) {
+      throw new IllegalArgumentException(url + " is a malformed URL.", e);
+    }
+  }
+
   /**
    * A base specification class for creating a new provider.
    *
@@ -68,7 +78,7 @@ public abstract class ProviderConfig {
      */
     public T setProviderId(String providerId) {
       checkArgument(
-          !Strings.isNullOrEmpty(providerId), "provider ID name must not be null or empty");
+          !Strings.isNullOrEmpty(providerId), "Provider ID name must not be null or empty.");
       this.providerId = providerId;
       return getThis();
     }
@@ -83,7 +93,7 @@ public abstract class ProviderConfig {
      * @param displayName a non-null, non-empty display name string.
      */
     public T setDisplayName(String displayName) {
-      checkArgument(!Strings.isNullOrEmpty(displayName), "display name must not be null or empty");
+      checkArgument(!Strings.isNullOrEmpty(displayName), "Display name must not be null or empty.");
       properties.put("displayName", displayName);
       return getThis();
     }
@@ -114,7 +124,7 @@ public abstract class ProviderConfig {
     final Map<String,Object> properties = new HashMap<>();
 
     AbstractUpdateRequest(String providerId) {
-      checkArgument(!Strings.isNullOrEmpty(providerId), "provider ID must not be null or empty");
+      checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty.");
       this.providerId = providerId;
     }
 
@@ -128,7 +138,7 @@ public abstract class ProviderConfig {
      * @param displayName a non-null, non-empty display name string.
      */
     public T setDisplayName(String displayName) {
-      checkArgument(!Strings.isNullOrEmpty(displayName), "display name must not be null or empty");
+      checkArgument(!Strings.isNullOrEmpty(displayName), "Display name must not be null or empty.");
       properties.put("displayName", displayName);
       return getThis();
     }


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 e069dd8..bbc61dd 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java
@@ -63,8 +63,9 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
-import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExternalResource;
 
 public class FirebaseAuthIT {
 
@@ -80,13 +81,11 @@ public class FirebaseAuthIT {
   private static final HttpTransport transport = Utils.getDefaultTransport();
   private static final String ACTION_LINK_CONTINUE_URL = "http://localhost/?a=1&b=2#c=3";
 
-  private static FirebaseAuth auth;
+  private static final FirebaseAuth auth = FirebaseAuth.getInstance(
+      IntegrationTestUtils.ensureDefaultApp());
 
-  @BeforeClass
-  public static void setUpClass() {
-    FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp();
-    auth = FirebaseAuth.getInstance(masterApp);
-  }
+  @Rule
+  public final TemporaryUser temporaryUser = new TemporaryUser();
 
   @Test
   public void testGetNonExistingUser() throws Exception {
@@ -149,28 +148,24 @@ public class FirebaseAuthIT {
         .setEmailVerified(true)
         .setPassword("password");
 
-    UserRecord userRecord = auth.createUserAsync(user).get();
-    try {
-      assertEquals(randomUser.uid, userRecord.getUid());
-      assertEquals("Random User", userRecord.getDisplayName());
-      assertEquals(randomUser.email, userRecord.getEmail());
-      assertEquals(phone, userRecord.getPhoneNumber());
-      assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
-      assertTrue(userRecord.isEmailVerified());
-      assertFalse(userRecord.isDisabled());
-
-      assertEquals(2, userRecord.getProviderData().length);
-      List<String> providers = new ArrayList<>();
-      for (UserInfo provider : userRecord.getProviderData()) {
-        providers.add(provider.getProviderId());
-      }
-      assertTrue(providers.contains("password"));
-      assertTrue(providers.contains("phone"));
+    UserRecord userRecord = temporaryUser.create(user);
+    assertEquals(randomUser.uid, userRecord.getUid());
+    assertEquals("Random User", userRecord.getDisplayName());
+    assertEquals(randomUser.email, userRecord.getEmail());
+    assertEquals(phone, userRecord.getPhoneNumber());
+    assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
+    assertTrue(userRecord.isEmailVerified());
+    assertFalse(userRecord.isDisabled());
 
-      checkRecreateUser(randomUser.uid);
-    } finally {
-      auth.deleteUserAsync(userRecord.getUid()).get();
+    assertEquals(2, userRecord.getProviderData().length);
+    List<String> providers = new ArrayList<>();
+    for (UserInfo provider : userRecord.getProviderData()) {
+      providers.add(provider.getProviderId());
     }
+    assertTrue(providers.contains("password"));
+    assertTrue(providers.contains("phone"));
+
+    checkRecreateUser(randomUser.uid);
   }
 
   @Test
@@ -247,227 +242,105 @@ public class FirebaseAuthIT {
   public void testListUsers() throws Exception {
     final List<String> uids = new ArrayList<>();
 
-    try {
-      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);
-      ListUsersPage page = auth.listUsersAsync(null).get();
-      while (page != null) {
-        for (ExportedUserRecord user : page.getValues()) {
-          if (uids.contains(user.getUid())) {
-            collected.incrementAndGet();
-            assertNotNull("Missing passwordHash field. A common cause would be "
-                + "forgetting to add the \"Firebase Authentication Admin\" permission. See "
-                + "instructions in CONTRIBUTING.md", user.getPasswordHash());
-            assertNotNull(user.getPasswordSalt());
-            assertNull(user.getTenantId());
-          }
-        }
-        page = page.getNextPage();
-      }
-      assertEquals(uids.size(), collected.get());
+    for (int i = 0; i < 3; i++) {
+      UserRecord.CreateRequest createRequest =
+          new UserRecord.CreateRequest().setPassword("password");
+      uids.add(temporaryUser.create(createRequest).getUid());
+    }
 
-      // Test iterate all
-      collected.set(0);
-      page = auth.listUsersAsync(null).get();
-      for (ExportedUserRecord user : page.iterateAll()) {
+    // Test list by batches
+    final AtomicInteger collected = new AtomicInteger(0);
+    ListUsersPage page = auth.listUsersAsync(null).get();
+    while (page != null) {
+      for (ExportedUserRecord user : page.getValues()) {
         if (uids.contains(user.getUid())) {
           collected.incrementAndGet();
-          assertNotNull(user.getPasswordHash());
+          assertNotNull("Missing passwordHash field. A common cause would be "
+              + "forgetting to add the \"Firebase Authentication Admin\" permission. See "
+              + "instructions in CONTRIBUTING.md", user.getPasswordHash());
           assertNotNull(user.getPasswordSalt());
           assertNull(user.getTenantId());
         }
       }
-      assertEquals(uids.size(), collected.get());
-
-      // Test iterate async
-      collected.set(0);
-      final Semaphore semaphore = new Semaphore(0);
-      final AtomicReference<Throwable> error = new AtomicReference<>();
-      ApiFuture<ListUsersPage> pageFuture = auth.listUsersAsync(null);
-      ApiFutures.addCallback(pageFuture, new ApiFutureCallback<ListUsersPage>() {
-        @Override
-        public void onFailure(Throwable t) {
-          error.set(t);
-          semaphore.release();
-        }
-
-        @Override
-        public void onSuccess(ListUsersPage result) {
-          for (ExportedUserRecord user : result.iterateAll()) {
-            if (uids.contains(user.getUid())) {
-              collected.incrementAndGet();
-              assertNotNull(user.getPasswordHash());
-              assertNotNull(user.getPasswordSalt());
-              assertNull(user.getTenantId());
-            }
-          }
-          semaphore.release();
-        }
-      }, MoreExecutors.directExecutor());
-      semaphore.acquire();
-      assertEquals(uids.size(), collected.get());
-      assertNull(error.get());
-    } finally {
-      for (String uid : uids) {
-        auth.deleteUserAsync(uid).get();
+      page = page.getNextPage();
+    }
+    assertEquals(uids.size(), collected.get());
+
+    // Test iterate all
+    collected.set(0);
+    page = auth.listUsersAsync(null).get();
+    for (ExportedUserRecord user : page.iterateAll()) {
+      if (uids.contains(user.getUid())) {
+        collected.incrementAndGet();
+        assertNotNull(user.getPasswordHash());
+        assertNotNull(user.getPasswordSalt());
+        assertNull(user.getTenantId());
       }
     }
-  }
-
-  @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());
+    assertEquals(uids.size(), collected.get());
+
+    // Test iterate async
+    collected.set(0);
+    final Semaphore semaphore = new Semaphore(0);
+    final AtomicReference<Throwable> error = new AtomicReference<>();
+    ApiFuture<ListUsersPage> pageFuture = auth.listUsersAsync(null);
+    ApiFutures.addCallback(pageFuture, new ApiFutureCallback<ListUsersPage>() {
+      @Override
+      public void onFailure(Throwable t) {
+        error.set(t);
+        semaphore.release();
       }
 
-      // 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())) {
+      @Override
+      public void onSuccess(ListUsersPage result) {
+        for (ExportedUserRecord user : result.iterateAll()) {
+          if (uids.contains(user.getUid())) {
             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());
-            }
+            assertNotNull(user.getPasswordHash());
+            assertNotNull(user.getPasswordSalt());
+            assertNull(user.getTenantId());
           }
-          semaphore.release();
         }
-      }, MoreExecutors.directExecutor());
-      semaphore.acquire();
-      assertEquals(tenantIds.size(), collected.get());
-      assertNull(error.get());
-    } finally {
-      for (String tenantId : tenantIds) {
-        tenantManager.deleteTenantAsync(tenantId).get();
+        semaphore.release();
       }
-    }
+    }, MoreExecutors.directExecutor());
+    semaphore.acquire();
+    assertEquals(uids.size(), collected.get());
+    assertNull(error.get());
   }
 
   @Test
   public void testCustomClaims() throws Exception {
-    UserRecord userRecord = auth.createUserAsync(new UserRecord.CreateRequest()).get();
+    UserRecord userRecord = temporaryUser.create(new UserRecord.CreateRequest());
     String uid = userRecord.getUid();
 
-    try {
-      // New user should not have any claims
-      assertTrue(userRecord.getCustomClaims().isEmpty());
-
-      Map<String, Object> expected = ImmutableMap.<String, Object>of(
-          "admin", true, "package", "gold");
-      auth.setCustomUserClaimsAsync(uid, expected).get();
-
-      // Should have 2 claims
-      UserRecord updatedUser = auth.getUserAsync(uid).get();
-      assertEquals(2, updatedUser.getCustomClaims().size());
-      for (Map.Entry<String, Object> entry : expected.entrySet()) {
-        assertEquals(entry.getValue(), updatedUser.getCustomClaims().get(entry.getKey()));
-      }
+    // New user should not have any claims
+    assertTrue(userRecord.getCustomClaims().isEmpty());
 
-      // User's ID token should have the custom claims
-      String customToken = auth.createCustomTokenAsync(uid).get();
-      String idToken = signInWithCustomToken(customToken);
-      FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get();
-      Map<String, Object> result = decoded.getClaims();
-      for (Map.Entry<String, Object> entry : expected.entrySet()) {
-        assertEquals(entry.getValue(), result.get(entry.getKey()));
-      }
+    Map<String, Object> expected = ImmutableMap.<String, Object>of(
+        "admin", true, "package", "gold");
+    auth.setCustomUserClaimsAsync(uid, expected).get();
 
-      // Should be able to remove custom claims
-      auth.setCustomUserClaimsAsync(uid, null).get();
-      updatedUser = auth.getUserAsync(uid).get();
-      assertTrue(updatedUser.getCustomClaims().isEmpty());
-    } finally {
-      auth.deleteUserAsync(uid).get();
+    // Should have 2 claims
+    UserRecord updatedUser = auth.getUserAsync(uid).get();
+    assertEquals(2, updatedUser.getCustomClaims().size());
+    for (Map.Entry<String, Object> entry : expected.entrySet()) {
+      assertEquals(entry.getValue(), updatedUser.getCustomClaims().get(entry.getKey()));
+    }
+
+    // User's ID token should have the custom claims
+    String customToken = auth.createCustomTokenAsync(uid).get();
+    String idToken = signInWithCustomToken(customToken);
+    FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get();
+    Map<String, Object> result = decoded.getClaims();
+    for (Map.Entry<String, Object> entry : expected.entrySet()) {
+      assertEquals(entry.getValue(), result.get(entry.getKey()));
     }
+
+    // Should be able to remove custom claims
+    auth.setCustomUserClaimsAsync(uid, null).get();
+    updatedUser = auth.getUserAsync(uid).get();
+    assertTrue(updatedUser.getCustomClaims().isEmpty());
   }
 
   @Test
@@ -586,15 +459,12 @@ public class FirebaseAuthIT {
         .build();
 
     UserImportResult result = auth.importUsersAsync(ImmutableList.of(user)).get();
+    temporaryUser.registerUid(randomUser.uid);
     assertEquals(1, result.getSuccessCount());
     assertEquals(0, result.getFailureCount());
 
-    try {
-      UserRecord savedUser = auth.getUserAsync(randomUser.uid).get();
-      assertEquals(randomUser.email, savedUser.getEmail());
-    } finally {
-      auth.deleteUserAsync(randomUser.uid).get();
-    }
+    UserRecord savedUser = auth.getUserAsync(randomUser.uid).get();
+    assertEquals(randomUser.email, savedUser.getEmail());
   }
 
   @Test
@@ -620,88 +490,73 @@ public class FirebaseAuthIT {
             .setRounds(8)
             .setMemoryCost(14)
             .build())).get();
+    temporaryUser.registerUid(randomUser.uid);
     assertEquals(1, result.getSuccessCount());
     assertEquals(0, result.getFailureCount());
 
-    try {
-      UserRecord savedUser = auth.getUserAsync(randomUser.uid).get();
-      assertEquals(randomUser.email, savedUser.getEmail());
-      String idToken = signInWithPassword(randomUser.email, "password");
-      assertFalse(Strings.isNullOrEmpty(idToken));
-    } finally {
-      auth.deleteUserAsync(randomUser.uid).get();
-    }
+    UserRecord savedUser = auth.getUserAsync(randomUser.uid).get();
+    assertEquals(randomUser.email, savedUser.getEmail());
+    String idToken = signInWithPassword(randomUser.email, "password");
+    assertFalse(Strings.isNullOrEmpty(idToken));
   }
 
   @Test
   public void testGeneratePasswordResetLink() throws Exception {
     RandomUser user = RandomUser.create();
-    auth.createUser(new UserRecord.CreateRequest()
+    temporaryUser.create(new UserRecord.CreateRequest()
         .setUid(user.uid)
         .setEmail(user.email)
         .setEmailVerified(false)
         .setPassword("password"));
-    try {
-      String link = auth.generatePasswordResetLink(user.email, ActionCodeSettings.builder()
-          .setUrl(ACTION_LINK_CONTINUE_URL)
-          .setHandleCodeInApp(false)
-          .build());
-      Map<String, String> linkParams = parseLinkParameters(link);
-      assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
-      String email = resetPassword(user.email, "password", "newpassword",
-          linkParams.get("oobCode"));
-      assertEquals(user.email, email);
-      // Password reset also verifies the user's email
-      assertTrue(auth.getUser(user.uid).isEmailVerified());
-    } finally {
-      auth.deleteUser(user.uid);
-    }
+    String link = auth.generatePasswordResetLink(user.email, ActionCodeSettings.builder()
+        .setUrl(ACTION_LINK_CONTINUE_URL)
+        .setHandleCodeInApp(false)
+        .build());
+    Map<String, String> linkParams = parseLinkParameters(link);
+    assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
+    String email = resetPassword(user.email, "password", "newpassword",
+        linkParams.get("oobCode"));
+    assertEquals(user.email, email);
+    // Password reset also verifies the user's email
+    assertTrue(auth.getUser(user.uid).isEmailVerified());
   }
 
   @Test
   public void testGenerateEmailVerificationResetLink() throws Exception {
     RandomUser user = RandomUser.create();
-    auth.createUser(new UserRecord.CreateRequest()
+    temporaryUser.create(new UserRecord.CreateRequest()
         .setUid(user.uid)
         .setEmail(user.email)
         .setEmailVerified(false)
         .setPassword("password"));
-    try {
-      String link = auth.generateEmailVerificationLink(user.email, ActionCodeSettings.builder()
-          .setUrl(ACTION_LINK_CONTINUE_URL)
-          .setHandleCodeInApp(false)
-          .build());
-      Map<String, String> linkParams = parseLinkParameters(link);
-      assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
-      // There doesn't seem to be a public API for verifying an email, so we cannot do a more
-      // thorough test here.
-      assertEquals("verifyEmail", linkParams.get("mode"));
-    } finally {
-      auth.deleteUser(user.uid);
-    }
+    String link = auth.generateEmailVerificationLink(user.email, ActionCodeSettings.builder()
+        .setUrl(ACTION_LINK_CONTINUE_URL)
+        .setHandleCodeInApp(false)
+        .build());
+    Map<String, String> linkParams = parseLinkParameters(link);
+    assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
+    // There doesn't seem to be a public API for verifying an email, so we cannot do a more
+    // thorough test here.
+    assertEquals("verifyEmail", linkParams.get("mode"));
   }
 
   @Test
   public void testGenerateSignInWithEmailLink() throws Exception {
     RandomUser user = RandomUser.create();
-    auth.createUser(new UserRecord.CreateRequest()
+    temporaryUser.create(new UserRecord.CreateRequest()
         .setUid(user.uid)
         .setEmail(user.email)
         .setEmailVerified(false)
         .setPassword("password"));
-    try {
-      String link = auth.generateSignInWithEmailLink(user.email, ActionCodeSettings.builder()
-          .setUrl(ACTION_LINK_CONTINUE_URL)
-          .setHandleCodeInApp(false)
-          .build());
-      Map<String, String> linkParams = parseLinkParameters(link);
-      assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
-      String idToken = signInWithEmailLink(user.email, linkParams.get("oobCode"));
-      assertFalse(Strings.isNullOrEmpty(idToken));
-      assertTrue(auth.getUser(user.uid).isEmailVerified());
-    } finally {
-      auth.deleteUser(user.uid);
-    }
+    String link = auth.generateSignInWithEmailLink(user.email, ActionCodeSettings.builder()
+        .setUrl(ACTION_LINK_CONTINUE_URL)
+        .setHandleCodeInApp(false)
+        .build());
+    Map<String, String> linkParams = parseLinkParameters(link);
+    assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
+    String idToken = signInWithEmailLink(user.email, linkParams.get("oobCode"));
+    assertFalse(Strings.isNullOrEmpty(idToken));
+    assertTrue(auth.getUser(user.uid).isEmailVerified());
   }
 
   @Test
@@ -991,4 +846,36 @@ public class FirebaseAuthIT {
           ((FirebaseAuthException) e.getCause()).getErrorCode());
     }
   }
+
+  /**
+   * Creates temporary Firebase user accounts for testing, and deletes them at the end of each
+   * test case.
+   */
+  private static final class TemporaryUser extends ExternalResource {
+
+    private final List<String> users = new ArrayList<>();
+
+    public UserRecord create(UserRecord.CreateRequest request) throws FirebaseAuthException {
+      UserRecord user = auth.createUser(request);
+      registerUid(user.getUid());
+      return user;
+    }
+
+    public synchronized void registerUid(String uid) {
+      users.add(uid);
+    }
+
+    @Override
+    protected synchronized void after() {
+      for (String uid : users) {
+        try {
+          auth.deleteUser(uid);
+        } catch (Exception ignore) {
+          // Ignore
+        }
+      }
+
+      users.clear();
+    }
+  }
 }
diff --git a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java
index 2c62e57..8079c5d 100644
--- a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java
+++ b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java
@@ -114,6 +114,11 @@ public class OidcProviderConfigTest {
     new OidcProviderConfig.UpdateRequest(null);
   }
 
+  @Test(expected = IllegalArgumentException.class)
+  public void testUpdateRequestInvalidProviderId() {
+    new OidcProviderConfig.UpdateRequest("saml.provider-id");
+  }
+
   @Test(expected = IllegalArgumentException.class)
   public void testUpdateRequestMissingClientId() {
     new OidcProviderConfig.UpdateRequest("oidc.provider-id").setClientId(null);
diff --git a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java
index 32572c5..ddfc9fe 100644
--- a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java
+++ b/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java
@@ -35,7 +35,6 @@ import com.google.api.client.json.JsonObjectParser;
 import com.google.api.core.ApiFuture;
 import com.google.api.core.ApiFutureCallback;
 import com.google.api.core.ApiFutures;
-import com.google.auth.oauth2.GoogleCredentials;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.firebase.FirebaseApp;
diff --git a/src/test/java/com/google/firebase/auth/TenantManagerIT.java b/src/test/java/com/google/firebase/auth/TenantManagerIT.java
new file mode 100644
index 0000000..6d96a11
--- /dev/null
+++ b/src/test/java/com/google/firebase/auth/TenantManagerIT.java
@@ -0,0 +1,155 @@
+/*
+ * 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 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.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.api.core.ApiFuture;
+import com.google.api.core.ApiFutureCallback;
+import com.google.api.core.ApiFutures;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.firebase.testing.IntegrationTestUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Test;
+
+public class TenantManagerIT {
+
+  private static final FirebaseAuth auth = FirebaseAuth.getInstance(
+      IntegrationTestUtils.ensureDefaultApp());
+
+  @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();
+      }
+    }
+  }
+}