Repository: firebase/firebase-admin-java
Pull request #372 (2020-02-20 21:37:55)

Add deleteTenant operation to TenantManager.
This pull request add deleteTenant to the TenantManager class. I've added the relevant unit tests to FirebaseUserManagerTest (based off of the getTenant unit test). This is part of the initiative to adding multi-tenancy support (see issue #332).

REST API: https://identitytoolkit.googleapis.com/$discovery/rest?version=v2


Patch:
diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
index eec3a81..4bab926 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java
@@ -65,6 +65,7 @@ import java.util.Map;
  */
 class FirebaseUserManager {
 
+  static final String TENANT_NOT_FOUND_ERROR = "tenant-not-found";
   static final String USER_NOT_FOUND_ERROR = "user-not-found";
   static final String INTERNAL_ERROR = "internal-error";
 
@@ -82,6 +83,7 @@ class FirebaseUserManager {
       .put("INVALID_PAGE_SELECTION", "invalid-page-token")
       .put("INVALID_PHONE_NUMBER", "invalid-phone-number")
       .put("PHONE_NUMBER_EXISTS", "phone-number-already-exists")
+      .put("TENANT_NOT_FOUND", TENANT_NOT_FOUND_ERROR)
       .put("PROJECT_NOT_FOUND", "project-not-found")
       .put("USER_NOT_FOUND", USER_NOT_FOUND_ERROR)
       .put("WEAK_PASSWORD", "invalid-password")
@@ -226,6 +228,15 @@ class FirebaseUserManager {
     return new UserImportResult(request.getUsersCount(), response);
   }
 
+  void deleteTenant(String tenantId) throws FirebaseAuthException {
+    GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants/" + tenantId);
+    GenericJson response = sendRequest("DELETE", url, null, GenericJson.class);
+    if (response == null) {
+      throw new FirebaseAuthException(TENANT_NOT_FOUND_ERROR,
+          "Failed to delete tenant: " + tenantId);
+    }
+  }
+
   ListTenantsResponse listTenants(int maxResults, String pageToken)
       throws FirebaseAuthException {
     ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder()
diff --git a/src/main/java/com/google/firebase/auth/TenantManager.java b/src/main/java/com/google/firebase/auth/TenantManager.java
index 62acd1b..1d1948c 100644
--- a/src/main/java/com/google/firebase/auth/TenantManager.java
+++ b/src/main/java/com/google/firebase/auth/TenantManager.java
@@ -16,10 +16,12 @@
 
 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.core.ApiFuture;
+import com.google.common.base.Strings;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.auth.ListTenantsPage.DefaultTenantSource;
 import com.google.firebase.auth.ListTenantsPage.PageFactory;
@@ -115,4 +117,40 @@ public final class TenantManager {
       }
     };
   }
+
+  /**
+   * Deletes the tenant identified by the specified tenant ID.
+   *
+   * @param tenantId A tenant ID string.
+   * @throws IllegalArgumentException If the tenant ID string is null or empty.
+   * @throws FirebaseAuthException If an error occurs while deleting the tenant.
+   */
+  public void deleteTenant(@NonNull String tenantId) throws FirebaseAuthException {
+    deleteTenantOp(tenantId).call();
+  }
+
+  /**
+   * Similar to {@link #deleteTenant(String)} but performs the operation asynchronously.
+   *
+   * @param tenantId A tenant ID string.
+   * @return An {@code ApiFuture} which will complete successfully when the specified tenant account
+   *     has been deleted. If an error occurs while deleting the tenant account, the future throws a
+   *     {@link FirebaseAuthException}.
+   * @throws IllegalArgumentException If the tenant ID string is null or empty.
+   */
+  public ApiFuture<Void> deleteTenantAsync(String tenantId) {
+    return deleteTenantOp(tenantId).callAsync(firebaseApp);
+  }
+
+  private CallableOperation<Void, FirebaseAuthException> deleteTenantOp(final String tenantId) {
+    // TODO(micahstairs): Add a check to make sure the app has not been destroyed yet.
+    checkArgument(!Strings.isNullOrEmpty(tenantId), "tenantId must not be null or empty");
+    return new CallableOperation<Void, FirebaseAuthException>() {
+      @Override
+      protected Void execute() throws FirebaseAuthException {
+        userManager.deleteTenant(tenantId);
+        return null;
+      }
+    };
+  }
 }


Test patch:
diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
index fa7de0c..c1ef26c 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java
@@ -16,10 +16,12 @@
 
 package com.google.firebase.auth;
 
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
 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.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -74,6 +76,8 @@ public class FirebaseUserManagerTest {
           .build();
   private static final Map<String, Object> ACTION_CODE_SETTINGS_MAP =
           ACTION_CODE_SETTINGS.getProperties();
+  private static final String TENANTS_BASE_URL =
+          "https://identitytoolkit.googleapis.com/v2/projects/test-project-id/tenants";
 
   @After
   public void tearDown() {
@@ -114,7 +118,7 @@ public class FirebaseUserManagerTest {
       FirebaseAuth.getInstance().getUserAsync("testuser").get();
       fail("No error thrown for invalid response");
     } catch (ExecutionException e) {
-      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertThat(e.getCause(), instanceOf(FirebaseAuthException.class));
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
       assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode());
     }
@@ -137,7 +141,7 @@ public class FirebaseUserManagerTest {
       FirebaseAuth.getInstance().getUserByEmailAsync("testuser@example.com").get();
       fail("No error thrown for invalid response");
     } catch (ExecutionException e) {
-      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertThat(e.getCause(), instanceOf(FirebaseAuthException.class));
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
       assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode());
     }
@@ -160,7 +164,7 @@ public class FirebaseUserManagerTest {
       FirebaseAuth.getInstance().getUserByPhoneNumberAsync("+1234567890").get();
       fail("No error thrown for invalid response");
     } catch (ExecutionException e) {
-      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertThat(e.getCause(), instanceOf(FirebaseAuthException.class));
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
       assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode());
     }
@@ -476,6 +480,32 @@ public class FirebaseUserManagerTest {
     checkRequestHeaders(interceptor);
   }
 
+  @Test
+  public void testDeleteTenant() throws Exception {
+    TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
+
+    FirebaseAuth.getInstance().getTenantManager().deleteTenantAsync("TENANT_1").get();
+
+    checkRequestHeaders(interceptor);
+    checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/TENANT_1");
+  }
+
+  @Test
+  public void testDeleteTenantWithNotFoundError() throws Exception {
+    TestResponseInterceptor interceptor =
+        initializeAppForUserManagementWithStatusCode(404,
+            "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}");
+    try {
+      FirebaseAuth.getInstance().getTenantManager().deleteTenantAsync("UNKNOWN").get();
+      fail("No error thrown for invalid response");
+    } catch (ExecutionException e) {
+      assertThat(e.getCause(), instanceOf(FirebaseAuthException.class));
+      FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
+      assertEquals(FirebaseUserManager.TENANT_NOT_FOUND_ERROR, authException.getErrorCode());
+    }
+    checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/UNKNOWN");
+  }
+
   @Test
   public void testCreateSessionCookie() throws Exception {
     TestResponseInterceptor interceptor = initializeAppForUserManagement(
@@ -615,11 +645,11 @@ public class FirebaseUserManagerTest {
           operation.call(FirebaseAuth.getInstance());
           fail("No error thrown for HTTP error: " + code);
         } catch (ExecutionException e) {
-          assertTrue(e.getCause() instanceof FirebaseAuthException);
+          assertThat(e.getCause(), instanceOf(FirebaseAuthException.class));
           FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
           String msg = String.format("Unexpected HTTP response with status: %d; body: {}", code);
           assertEquals(msg, authException.getMessage());
-          assertTrue(authException.getCause() instanceof HttpResponseException);
+          assertThat(authException.getCause(), instanceOf(HttpResponseException.class));
           assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode());
         }
       }
@@ -633,10 +663,10 @@ public class FirebaseUserManagerTest {
         operation.call(FirebaseAuth.getInstance());
         fail("No error thrown for HTTP error");
       }  catch (ExecutionException e) {
-        assertTrue(e.getCause().toString(), e.getCause() instanceof FirebaseAuthException);
+        assertThat(e.getCause().toString(), e.getCause(), instanceOf(FirebaseAuthException.class));
         FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
         assertEquals("User management service responded with an error", authException.getMessage());
-        assertTrue(authException.getCause() instanceof HttpResponseException);
+        assertThat(authException.getCause(), instanceOf(HttpResponseException.class));
         assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode());
       }
     }
@@ -649,33 +679,23 @@ public class FirebaseUserManagerTest {
       FirebaseAuth.getInstance().getUserAsync("testuser").get();
       fail("No error thrown for JSON error");
     }  catch (ExecutionException e) {
-      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertThat(e.getCause(), instanceOf(FirebaseAuthException.class));
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertTrue(authException.getCause() instanceof IOException);
+      assertThat(authException.getCause(), instanceOf(IOException.class));
       assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode());
     }
   }
 
   @Test
   public void testGetUserUnexpectedHttpError() throws Exception {
-    MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
-    response.setContent("{\"not\" json}");
-    response.setStatusCode(500);
-    MockHttpTransport transport = new MockHttpTransport.Builder()
-        .setLowLevelHttpResponse(response)
-        .build();
-    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
-        .setCredentials(credentials)
-        .setProjectId("test-project-id")
-        .setHttpTransport(transport)
-        .build());
+    initializeAppForUserManagementWithStatusCode(500, "{\"not\" json}");
     try {
       FirebaseAuth.getInstance().getUserAsync("testuser").get();
       fail("No error thrown for JSON error");
     }  catch (ExecutionException e) {
-      assertTrue(e.getCause() instanceof FirebaseAuthException);
+      assertThat(e.getCause(), instanceOf(FirebaseAuthException.class));
       FirebaseAuthException authException = (FirebaseAuthException) e.getCause();
-      assertTrue(authException.getCause() instanceof HttpResponseException);
+      assertThat(authException.getCause(), instanceOf(HttpResponseException.class));
       assertEquals("Unexpected HTTP response with status: 500; body: {\"not\" json}",
           authException.getMessage());
       assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode());
@@ -1219,47 +1239,46 @@ public class FirebaseUserManagerTest {
 
   @Test
   public void testHttpErrorWithCode() {
-    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
-        .setCredentials(credentials)
-        .setHttpTransport(new MultiRequestMockHttpTransport(ImmutableList.of(
-            new MockLowLevelHttpResponse()
-                .setContent("{\"error\": {\"message\": \"UNAUTHORIZED_DOMAIN\"}}")
-                .setStatusCode(500))))
-        .setProjectId("test-project-id")
-        .build());
-    FirebaseAuth auth = FirebaseAuth.getInstance();
-    FirebaseUserManager userManager = auth.getUserManager();
+    initializeAppForUserManagementWithStatusCode(500,
+        "{\"error\": {\"message\": \"UNAUTHORIZED_DOMAIN\"}}");
+    FirebaseUserManager userManager = FirebaseAuth.getInstance().getUserManager();
     try {
       userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null);
       fail("No exception thrown for HTTP error");
     } catch (FirebaseAuthException e) {
       assertEquals("unauthorized-continue-uri", e.getErrorCode());
-      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertThat(e.getCause(), instanceOf(HttpResponseException.class));
     }
   }
 
   @Test
   public void testUnexpectedHttpError() {
-    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
-        .setCredentials(credentials)
-        .setHttpTransport(new MultiRequestMockHttpTransport(ImmutableList.of(
-            new MockLowLevelHttpResponse()
-                .setContent("{}")
-                .setStatusCode(500))))
-        .setProjectId("test-project-id")
-        .build());
-    FirebaseAuth auth = FirebaseAuth.getInstance();
-    FirebaseUserManager userManager = auth.getUserManager();
+    initializeAppForUserManagementWithStatusCode(500, "{}");
+    FirebaseUserManager userManager = FirebaseAuth.getInstance().getUserManager();
     try {
       userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null);
       fail("No exception thrown for HTTP error");
     } catch (FirebaseAuthException e) {
       assertEquals("internal-error", e.getErrorCode());
-      assertTrue(e.getCause() instanceof HttpResponseException);
+      assertThat(e.getCause(), instanceOf(HttpResponseException.class));
     }
   }
 
-  private static TestResponseInterceptor initializeAppForUserManagement(String ...responses) {
+  private static TestResponseInterceptor initializeAppForUserManagementWithStatusCode(
+      int statusCode, String response) {
+    FirebaseApp.initializeApp(new FirebaseOptions.Builder()
+        .setCredentials(credentials)
+        .setHttpTransport(
+          MockHttpTransport.builder().setLowLevelHttpResponse(
+            new MockLowLevelHttpResponse().setContent(response).setStatusCode(statusCode)).build())
+        .setProjectId("test-project-id")
+        .build());
+    TestResponseInterceptor interceptor = new TestResponseInterceptor();
+    FirebaseAuth.getInstance().getUserManager().setInterceptor(interceptor);
+    return interceptor;
+  }
+
+  private static TestResponseInterceptor initializeAppForUserManagement(String... responses) {
     List<MockLowLevelHttpResponse> mocks = new ArrayList<>();
     for (String response : responses) {
       mocks.add(new MockLowLevelHttpResponse().setContent(response));
@@ -1270,10 +1289,8 @@ public class FirebaseUserManagerTest {
         .setHttpTransport(transport)
         .setProjectId("test-project-id")
         .build());
-    FirebaseAuth auth = FirebaseAuth.getInstance();
-    FirebaseUserManager userManager = auth.getUserManager();
     TestResponseInterceptor interceptor = new TestResponseInterceptor();
-    userManager.setInterceptor(interceptor);
+    FirebaseAuth.getInstance().getUserManager().setInterceptor(interceptor);
     return interceptor;
   }
 
@@ -1326,6 +1343,12 @@ public class FirebaseUserManagerTest {
     assertEquals(clientVersion, headers.getFirstHeaderStringValue("X-Client-Version"));
   }
 
+  private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) {
+    HttpRequest request = interceptor.getResponse().getRequest();
+    assertEquals(method, request.getRequestMethod());
+    assertEquals(url, request.getUrl().toString());
+  }
+
   private interface UserManagerOp {
     void call(FirebaseAuth auth) throws Exception;
   }