/* * Copyright 2017 Google Inc. * * 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; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.google.common.base.Defaults; import com.google.common.io.BaseEncoding; import com.google.firebase.FirebaseApp.Clock; import com.google.firebase.FirebaseApp.TokenRefresher; import com.google.firebase.FirebaseOptions.Builder; import com.google.firebase.auth.FirebaseCredential; import com.google.firebase.auth.FirebaseCredentials; import com.google.firebase.auth.GoogleOAuthAccessToken; import com.google.firebase.database.FirebaseDatabase; import com.google.firebase.internal.AuthStateListener; import com.google.firebase.internal.GetTokenResult; import com.google.firebase.tasks.Task; import com.google.firebase.tasks.TaskCompletionSource; import com.google.firebase.tasks.Tasks; import com.google.firebase.testing.FirebaseAppRule; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.mockito.Mockito; /** * Unit tests for {@link com.google.firebase.FirebaseApp}. */ // TODO: uncomment lines when Firebase API targets are in integ. public class FirebaseAppTest { private static final FirebaseOptions OPTIONS = new FirebaseOptions.Builder() .setCredential(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .build(); private static final FirebaseOptions MOCK_CREDENTIAL_OPTIONS = new Builder().setCredential(new MockFirebaseCredential()).build(); @Rule public FirebaseAppRule firebaseAppRule = new FirebaseAppRule(); private static void invokePublicInstanceMethodWithDefaultValues(Object instance, Method method) throws InvocationTargetException, IllegalAccessException { List<Object> parameters = new ArrayList<>(method.getParameterTypes().length); for (Class<?> parameterType : method.getParameterTypes()) { parameters.add(Defaults.defaultValue(parameterType)); } method.invoke(instance, parameters.toArray()); } @Test(expected = NullPointerException.class) public void testNullAppName() { FirebaseApp.initializeApp(OPTIONS, null); } @Test(expected = IllegalArgumentException.class) public void testEmptyAppName() { FirebaseApp.initializeApp(OPTIONS, ""); } @Test(expected = IllegalStateException.class) public void testGetInstancePersistedNotInitialized() { String name = "myApp"; FirebaseApp.initializeApp(OPTIONS, name); TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); FirebaseApp.getInstance(name); } @Test(expected = IllegalStateException.class) public void testRehydratingDeletedInstanceThrows() { final String name = "myApp"; FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS, name); firebaseApp.delete(); TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); FirebaseApp.getInstance(name); } @Test public void testDeleteDefaultApp() { FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS); assertEquals(firebaseApp, FirebaseApp.getInstance()); firebaseApp.delete(); try { FirebaseApp.getInstance(); fail(); } catch (IllegalStateException expected) { // ignore } finally { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); } } @Test public void testDeleteApp() { final String name = "myApp"; FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS, name); assertSame(firebaseApp, FirebaseApp.getInstance(name)); firebaseApp.delete(); try { FirebaseApp.getInstance(name); fail(); } catch (IllegalStateException expected) { // ignore } try { // Verify we can reuse the same app name. FirebaseApp firebaseApp2 = FirebaseApp.initializeApp(OPTIONS, name); assertSame(firebaseApp2, FirebaseApp.getInstance(name)); assertNotSame(firebaseApp, firebaseApp2); } finally { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); } } @Test public void testGetApps() { FirebaseApp app1 = FirebaseApp.initializeApp(OPTIONS, "app1"); FirebaseApp app2 = FirebaseApp.initializeApp(OPTIONS, "app2"); List<FirebaseApp> apps = FirebaseApp.getApps(); assertEquals(2, apps.size()); assertTrue(apps.contains(app1)); assertTrue(apps.contains(app2)); } @Test public void testGetNullApp() { FirebaseApp app1 = FirebaseApp.initializeApp(OPTIONS, "app"); try { FirebaseApp.getInstance(null); fail("Not thrown"); } catch (NullPointerException expected) { // ignore } } @Test public void testToString() throws IOException { FirebaseOptions options = new FirebaseOptions.Builder() .setCredential(FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream())) .build(); FirebaseApp app = FirebaseApp.initializeApp(options, "app"); String pattern = "FirebaseApp\\{name=app, options=FirebaseOptions\\{" + "databaseUrl=null, credential=[^\\s]+, databaseAuthVariableOverride=\\{}}}"; assertTrue(app.toString().matches(pattern)); } @Test public void testInvokeAfterDeleteThrows() throws Exception { // delete and hidden methods shouldn't throw even after delete. Collection<String> allowedToCallAfterDelete = Arrays.asList( "addAuthStateChangeListener", "addBackgroundStateChangeListener", "delete", "equals", "getListeners", "getPersistenceKey", "hashCode", "isDefaultApp", "notifyAuthStateListeners", "removeAuthStateChangeListener", "removeBackgroundStateChangeListener", "setTokenProvider", "toString"); FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS, "myApp"); firebaseApp.delete(); for (Method method : firebaseApp.getClass().getDeclaredMethods()) { int modifiers = method.getModifiers(); if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers)) { try { if (!allowedToCallAfterDelete.contains(method.getName())) { invokePublicInstanceMethodWithDefaultValues(firebaseApp, method); fail("Method expected to throw, but didn't " + method.getName()); } } catch (InvocationTargetException e) { if (!(e.getCause() instanceof IllegalStateException) || e.getCause().getMessage().equals("FirebaseApp was deleted.")) { fail( "Expected FirebaseApp#" + method.getName() + " to throw " + "IllegalStateException with message \"FirebaseApp was deleted\", " + "but instead got " + e.getCause()); } } } } } @Test public void testPersistenceKey() { String name = "myApp"; FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS, name); String persistenceKey = firebaseApp.getPersistenceKey(); assertEquals(name, new String(BaseEncoding.base64Url().omitPadding().decode(persistenceKey), UTF_8)); } // Order of test cases matters. @Test(expected = IllegalStateException.class) public void testMissingInit() { FirebaseDatabase.getInstance(); } @Test public void testApiInitForNonDefaultApp() { FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS, "myApp"); assertFalse(ImplFirebaseTrampolines.isDefaultApp(firebaseApp)); } @Test public void testApiInitForDefaultApp() { // Explicit initialization of FirebaseApp instance. FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS); assertTrue(ImplFirebaseTrampolines.isDefaultApp(firebaseApp)); } @Test public void testTokenCaching() throws ExecutionException, InterruptedException, IOException { FirebaseApp firebaseApp = FirebaseApp.initializeApp(MOCK_CREDENTIAL_OPTIONS, "myApp"); GetTokenResult token1 = Tasks.await(TestOnlyImplFirebaseTrampolines.getToken( firebaseApp, false)); GetTokenResult token2 = Tasks.await(TestOnlyImplFirebaseTrampolines.getToken( firebaseApp, false)); Assert.assertNotNull(token1); Assert.assertNotNull(token2); Assert.assertEquals(token1, token2); } @Test public void testTokenForceRefresh() throws ExecutionException, InterruptedException, IOException { FirebaseApp firebaseApp = FirebaseApp.initializeApp(MOCK_CREDENTIAL_OPTIONS, "myApp"); GetTokenResult token1 = Tasks.await(TestOnlyImplFirebaseTrampolines.getToken( firebaseApp, false)); GetTokenResult token2 = Tasks.await(TestOnlyImplFirebaseTrampolines.getToken( firebaseApp, true)); Assert.assertNotNull(token1); Assert.assertNotNull(token2); Assert.assertNotEquals(token1, token2); } @Test public void testTokenExpiration() throws ExecutionException, InterruptedException, IOException { TestClock clock = new TestClock(); FirebaseApp firebaseApp = FirebaseApp.initializeApp(new Builder() .setCredential(new ClockedMockFirebaseCredential(clock)).build(), "myApp", FirebaseApp.DEFAULT_TOKEN_REFRESHER_FACTORY, clock); GetTokenResult token1 = Tasks.await(TestOnlyImplFirebaseTrampolines.getToken( firebaseApp, false)); GetTokenResult token2 = Tasks.await(TestOnlyImplFirebaseTrampolines.getToken( firebaseApp, false)); Assert.assertNotNull(token1); Assert.assertNotNull(token2); Assert.assertEquals(token1, token2); clock.timestamp += TimeUnit.HOURS.toMillis(1); GetTokenResult token3 = Tasks.await(TestOnlyImplFirebaseTrampolines.getToken( firebaseApp, false)); Assert.assertNotNull(token3); Assert.assertNotEquals(token1, token3); } @Test public void testAddAuthStateListenerWithoutInitialToken() { FirebaseApp firebaseApp = FirebaseApp.initializeApp(MOCK_CREDENTIAL_OPTIONS, "myApp"); AuthStateListener listener = mock(AuthStateListener.class); firebaseApp.addAuthStateListener(listener); verify(listener, never()).onAuthStateChanged(Mockito.any(GetTokenResult.class)); } @Test public void testAuthStateListenerAddWithInitialToken() throws Exception { FirebaseApp firebaseApp = FirebaseApp.initializeApp(MOCK_CREDENTIAL_OPTIONS, "myApp"); Tasks.await(firebaseApp.getToken(true)); final TaskCompletionSource<Boolean> completionSource = new TaskCompletionSource<>(); AuthStateListener listener = new AuthStateListener() { @Override public void onAuthStateChanged(GetTokenResult tokenResult) { completionSource.setResult(true); } }; firebaseApp.addAuthStateListener(listener); Tasks.await(completionSource.getTask()); assertTrue(completionSource.getTask().isSuccessful()); } @Test public void testAuthStateListenerOnTokenChange() throws Exception { FirebaseApp firebaseApp = FirebaseApp.initializeApp(MOCK_CREDENTIAL_OPTIONS, "myApp"); AuthStateListener listener = mock(AuthStateListener.class); firebaseApp.addAuthStateListener(listener); for (int i = 0; i < 5; i++) { Tasks.await(firebaseApp.getToken(true)); verify(listener, times(i + 1)).onAuthStateChanged(Mockito.any(GetTokenResult.class)); } } @Test public void testAuthStateListenerWithNoRefresh() throws Exception { FirebaseApp firebaseApp = FirebaseApp.initializeApp(MOCK_CREDENTIAL_OPTIONS, "myApp"); AuthStateListener listener = mock(AuthStateListener.class); firebaseApp.addAuthStateListener(listener); Tasks.await(firebaseApp.getToken(true)); verify(listener, times(1)).onAuthStateChanged(Mockito.any(GetTokenResult.class)); reset(listener); Tasks.await(firebaseApp.getToken(false)); verify(listener, never()).onAuthStateChanged(Mockito.any(GetTokenResult.class)); } @Test public void testAuthStateListenerRemoval() throws Exception { FirebaseApp firebaseApp = FirebaseApp.initializeApp(MOCK_CREDENTIAL_OPTIONS, "myApp"); AuthStateListener listener = mock(AuthStateListener.class); firebaseApp.addAuthStateListener(listener); Tasks.await(firebaseApp.getToken(true)); verify(listener, times(1)).onAuthStateChanged(Mockito.any(GetTokenResult.class)); reset(listener); firebaseApp.removeAuthStateListener(listener); Tasks.await(firebaseApp.getToken(false)); verify(listener, never()).onAuthStateChanged(Mockito.any(GetTokenResult.class)); } @Test public void testProactiveTokenRefresh() throws Exception { MockTokenRefresherFactory factory = new MockTokenRefresherFactory(); FirebaseApp firebaseApp = FirebaseApp.initializeApp(MOCK_CREDENTIAL_OPTIONS, "myApp", factory, FirebaseApp.DEFAULT_CLOCK); MockTokenRefresher tokenRefresher = factory.instance; Assert.assertNotNull(tokenRefresher); AuthStateListener listener = mock(AuthStateListener.class); firebaseApp.addAuthStateListener(listener); Tasks.await(firebaseApp.getToken(true)); verify(listener, times(1)).onAuthStateChanged(Mockito.any(GetTokenResult.class)); tokenRefresher.simulateDelay(55); verify(listener, times(2)).onAuthStateChanged(Mockito.any(GetTokenResult.class)); tokenRefresher.simulateDelay(20); verify(listener, times(2)).onAuthStateChanged(Mockito.any(GetTokenResult.class)); tokenRefresher.simulateDelay(35); verify(listener, times(3)).onAuthStateChanged(Mockito.any(GetTokenResult.class)); } @Test(expected = IllegalArgumentException.class) public void testFirebaseExceptionNullDetail() { new FirebaseException(null); } @Test(expected = IllegalArgumentException.class) public void testFirebaseExceptionEmptyDetail() { new FirebaseException(""); } private static class MockFirebaseCredential implements FirebaseCredential { @Override public Task<GoogleOAuthAccessToken> getAccessToken() { return Tasks.forResult(new GoogleOAuthAccessToken(UUID.randomUUID().toString(), System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1))); } } private static class ClockedMockFirebaseCredential implements FirebaseCredential { private final TestClock clock; ClockedMockFirebaseCredential(TestClock clock) { this.clock = clock; } @Override public Task<GoogleOAuthAccessToken> getAccessToken() { return Tasks.forResult(new GoogleOAuthAccessToken(UUID.randomUUID().toString(), clock.now() + TimeUnit.HOURS.toMillis(1))); } } private static class MockTokenRefresher extends TokenRefresher { private Callable<Task<GetTokenResult>> task; private long executeAt; private long time; MockTokenRefresher(FirebaseApp app) { super(app); } @Override protected void cancelPrevious() { task = null; } @Override protected void scheduleNext(Callable<Task<GetTokenResult>> task, long delayMillis) { this.task = task; executeAt = time + delayMillis; } /** * Simulates passage of time. Advances the clock, and runs the scheduled task if exists. Also * waits for the execution of any initiated tasks. * * @param delayMinutes Duration in minutes to advance the clock by */ void simulateDelay(int delayMinutes) throws Exception { Task<GetTokenResult> refreshTask = null; synchronized (this) { time += TimeUnit.MINUTES.toMillis(delayMinutes); if (task != null && time >= executeAt) { refreshTask = task.call(); } } if (refreshTask != null) { Tasks.await(refreshTask); } } } private static class MockTokenRefresherFactory extends TokenRefresher.Factory { MockTokenRefresher instance; @Override TokenRefresher create(FirebaseApp app) { instance = new MockTokenRefresher(app); return instance; } } private static class TestClock extends FirebaseApp.Clock { private long timestamp; @Override long now() { return timestamp; } } }