diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index eec3a81cc..4bab926e3 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 @@ */ 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 @@ UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthExcep 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 builder = ImmutableMap.builder() diff --git a/src/main/java/com/google/firebase/auth/TenantManager.java b/src/main/java/com/google/firebase/auth/TenantManager.java index 62acd1b64..1d1948c30 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 @@ protected ListTenantsPage execute() throws FirebaseAuthException { } }; } + + /** + * 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 deleteTenantAsync(String tenantId) { + return deleteTenantOp(tenantId).callAsync(firebaseApp); + } + + private CallableOperation 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() { + @Override + protected Void execute() throws FirebaseAuthException { + userManager.deleteTenant(tenantId); + return null; + } + }; + } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index fa7de0c8e..c1ef26c86 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 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 void testGetUserWithNotFoundError() throws Exception { 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 void testGetUserByEmailWithNotFoundError() throws Exception { 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 void testGetUserByPhoneNumberWithNotFoundError() throws Exception { 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 void testListZeroTenants() throws Exception { 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 void call(FirebaseAuth auth) throws Exception { 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 void call(FirebaseAuth auth) throws Exception { 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 void testGetUserMalformedJsonError() throws Exception { 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 void testGenerateSignInWithEmailLinkWithSettings() throws Exception { @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 mocks = new ArrayList<>(); for (String response : responses) { mocks.add(new MockLowLevelHttpResponse().setContent(response)); @@ -1270,10 +1289,8 @@ private static TestResponseInterceptor initializeAppForUserManagement(String ... .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 @@ private static void checkRequestHeaders(TestResponseInterceptor interceptor) { 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; }