diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle index 0ef070e8b6d..428083c4939 100644 --- a/firebase-installations/firebase-installations.gradle +++ b/firebase-installations/firebase-installations.gradle @@ -53,7 +53,8 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation "org.robolectric:robolectric:$robolectricVersion" testImplementation "com.google.truth:truth:$googleTruthVersion" - + testImplementation 'org.mockito:mockito-core:2.25.0' + testImplementation 'org.mockito:mockito-inline:2.25.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:runner:1.2.0' @@ -61,5 +62,5 @@ dependencies { androidTestImplementation 'junit:junit:4.12' androidTestImplementation "androidx.annotation:annotation:1.0.0" androidTestImplementation 'org.mockito:mockito-core:2.25.0' - androidTestImplementation 'org.mockito:mockito-android:2.25.0' + androidTestImplementation 'org.mockito:mockito-inline:2.25.0' } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java deleted file mode 100644 index 4fec8c4cce5..00000000000 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ /dev/null @@ -1,849 +0,0 @@ -// Copyright 2019 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.installations; - -import static com.google.common.truth.Truth.assertWithMessage; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_API_KEY; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_2; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_3; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_4; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_2; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE_WITH_IID; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTANCE_ID_1; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_PROJECT_ID; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_RESULT; -import static com.google.firebase.installations.local.PersistedInstallationEntrySubject.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.matches; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -import androidx.test.core.app.ApplicationProvider; -import androidx.test.runner.AndroidJUnit4; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.Tasks; -import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseException; -import com.google.firebase.FirebaseOptions; -import com.google.firebase.installations.FirebaseInstallationsException.Status; -import com.google.firebase.installations.local.IidStore; -import com.google.firebase.installations.local.PersistedInstallation; -import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; -import com.google.firebase.installations.local.PersistedInstallationEntry; -import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; -import com.google.firebase.installations.remote.InstallationResponse; -import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; -import com.google.firebase.installations.remote.TokenResult; -import java.io.IOException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import org.junit.After; -import org.junit.Before; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.MethodSorters; -import org.mockito.AdditionalAnswers; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class FirebaseInstallationsInstrumentedTest { - private FirebaseApp firebaseApp; - private ExecutorService executor; - private PersistedInstallation persistedInstallation; - @Mock private FirebaseInstallationServiceClient mockBackend; - @Mock private IidStore mockIidStore; - @Mock private RandomFidGenerator mockFidGenerator; - - private static final PersistedInstallationEntry REGISTERED_INSTALLATION_ENTRY = - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_FID_1) - .setAuthToken(TEST_AUTH_TOKEN) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) - .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) - .build(); - - private FirebaseInstallations firebaseInstallations; - private Utils utils; - private FakeCalendar fakeCalendar; - - @Before - public void setUp() throws FirebaseException, IOException { - MockitoAnnotations.initMocks(this); - FirebaseApp.clearInstancesForTest(); - executor = new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); - fakeCalendar = new FakeCalendar(5000000L); - firebaseApp = - FirebaseApp.initializeApp( - ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder() - .setApplicationId(TEST_APP_ID_1) - .setProjectId(TEST_PROJECT_ID) - .setApiKey(TEST_API_KEY) - .build()); - persistedInstallation = new PersistedInstallation(firebaseApp); - persistedInstallation.clearForTesting(); - - utils = new Utils(fakeCalendar); - firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - mockBackend, - persistedInstallation, - utils, - mockIidStore, - mockFidGenerator); - - when(mockFidGenerator.createRandomFid()).thenReturn(TEST_FID_1); - } - - @After - public void cleanUp() { - persistedInstallation.clearForTesting(); - try { - executor.awaitTermination(250, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - - } - } - - /** - * Check the id generation process when there is no network. There are three cases: - * - * - */ - @Test - public void testGetId_noNetwork_noIid() throws Exception { - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - when(mockIidStore.readIid()).thenReturn(null); - - // Do the actual getId() call under test. Confirm that it returns a generated FID and - // and that the FID was written to storage. - // Confirm both that it returns the expected ID, as does reading the prefs from storage. - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // The storage should still have the same ID and the status should indicate that the - // fid is registered. - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); - } - - @Test - public void testGetId_noNetwork_iidPresent() throws Exception { - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); - - // Do the actual getId() call under test. Confirm that it returns a generated FID and - // and that the FID was written to storage. - // Confirm both that it returns the expected ID, as does reading the prefs from storage. - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_INSTANCE_ID_1); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_INSTANCE_ID_1); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // The storage should still have the same ID and the status should indicate that the - // fid is registered. - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_INSTANCE_ID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); - } - - @Test - public void testGetId_noNetwork_fidAlreadyGenerated() throws Exception { - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid("generatedFid")); - - // Do the actual getId() call under test. Confirm that it returns the already generated FID. - // Confirm both that it returns the expected ID, as does reading the prefs from storage. - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo("generatedFid"); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // The storage should still have the same ID and the status should indicate that the - // fid is registered. - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid("generatedFid"); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); - } - - /** - * Checks that if we have a registered fid then the fid is returned and no backend calls are made. - */ - @Test - public void testGetId_ValidIdAndToken_NoBackendCalls() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // No exception, means success. - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - - // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second - // while we mock fid registration. We dont send an actual request to FIS in tests. - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // check that the mockClient didn't get invoked at all, since the fid is already registered - // and the authtoken is present and not expired - verifyZeroInteractions(mockBackend); - - // check that the fid is still the expected one and is registered - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); - } - - /** - * Checks that if we have an unregistered fid that the fid gets registered with the backend and no - * other calls are made. - */ - @Test - public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception { - when(mockBackend.createFirebaseInstallation( - anyString(), matches(TEST_FID_1), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - - // No exception, means success. - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - - // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second - // while we mock fid registration. We dont send an actual request to FIS in tests. - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // check that the mockClient didn't get invoked at all, since the fid is already registered - // and the authtoken is present and not expired - verify(mockBackend) - .createFirebaseInstallation(anyString(), matches(TEST_FID_1), anyString(), anyString()); - verify(mockBackend, never()) - .generateAuthToken(anyString(), anyString(), anyString(), anyString()); - - // check that the fid is still the expected one and is registered - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); - } - - @Test - public void testGetId_migrateIid_successful() throws Exception { - when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID); - - // Do the actual getId() call under test. - // Confirm both that it returns the expected ID, as does reading the prefs from storage. - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_INSTANCE_ID_1); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_INSTANCE_ID_1); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // The storage should still have the same ID and the status should indicate that the - // fid si registered. - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_INSTANCE_ID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); - } - - @Test - public void testGetId_multipleCalls_sameFIDReturned() throws Exception { - when(mockIidStore.readIid()).thenReturn(null); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - - // Call getId multiple times - Task task1 = firebaseInstallations.getId(); - Task task2 = firebaseInstallations.getId(); - Tasks.await(Tasks.whenAllComplete(task1, task2)); - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - assertWithMessage("Persisted Fid of Task1 doesn't match.") - .that(task1.getResult()) - .isEqualTo(TEST_FID_1); - assertWithMessage("Persisted Fid of Task2 doesn't match.") - .that(task2.getResult()) - .isEqualTo(TEST_FID_1); - verify(mockBackend, times(1)) - .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); - } - - /** - * Checks that if the server rejects a FID during registration the SDK will use the fid in the - * response as the new fid. - */ - @Test - public void testGetId_unregistered_replacesFidWithResponse() throws Exception { - // Update local storage with installation entry that has invalid fid. - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid("tobereplaced")); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - - // The first call will return the existing FID, "tobereplaced" - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo("tobereplaced"); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // The next call should return the FID that was returned by the server - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - } - - /** - * A registration that fails with a SERVER_ERROR will cause the FID to be put into the error - * state. - */ - @Test - public void testGetId_ServerError_UnregisteredFID() throws Exception { - // start with an unregistered fid - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - - // have the server return a server error for the registration - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn( - InstallationResponse.builder().setResponseCode(ResponseCode.BAD_CONFIG).build()); - - // do a getId(), the unregistered TEST_FID_1 should be returned - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - - // Waiting for Task that registers FID on the FIS Servers. - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // We expect that the server error will cause the FID to be put into the error state. - // There is nothing more we can do. - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); - } - - /** - * A registration that fails with an IOException will not cause the FID to be put into the error - * state. - */ - @Test - public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws Exception { - // set initial state to having an unregistered FID - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - - // Mocking unchecked exception on FIS createFirebaseInstallation - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - - String fid = Tasks.await(firebaseInstallations.getId()); - assertEquals("fid doesn't match expected", TEST_FID_1, fid); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // We expect that the IOException will cause the request to fail, but it will not - // cause the FID to be put into the error state because we expect this to eventually succeed. - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); - } - - @Test - public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws Exception { - // Start with a registered FID - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // Move the time forward by the token expiration time. - fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); - - // Mocking unchecked exception on FIS generateAuthToken - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - - assertWithMessage("getId Task failed") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - - // Waiting for Task that generates auth token with the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // Validate that registration status is still REGISTER - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); - } - - /** - * The FID is successfully registered but the token is expired. A getId will cause the token to be - * refreshed in the background. - */ - @Test - public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { - // Start with a registered FID - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // Make the server generateAuthToken() call return a refreshed token - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_TOKEN_RESULT); - - // Move the time forward by the token expiration time. - fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); - - // Get the ID, which should cause the SDK to realize that the auth token is expired and - // kick off a refresh of the token. - assertWithMessage("getId Task failed") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // Check that the token has been refreshed - assertWithMessage("auth token is not what is expected after the refresh") - .that( - Tasks.await( - firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)) - .getToken()) - .isEqualTo(TEST_AUTH_TOKEN_2); - - verify(mockBackend, never()) - .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); - verify(mockBackend, times(1)) - .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); - } - - @Test - public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); - - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasAuthToken(TEST_AUTH_TOKEN); - } - - @Test - public void testGetAuthToken_fidExists_successful() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - InstallationTokenResult installationTokenResult = - Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); - - assertWithMessage("Persisted Auth Token doesn't match") - .that(installationTokenResult.getToken()) - .isEqualTo(TEST_AUTH_TOKEN); - verify(mockBackend, never()) - .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); - } - - @Test - public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { - // start with a registered FID and valid auth token - persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - - // Move the time forward by the token expiration time. - fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); - - // have the server respond with a new token - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_TOKEN_RESULT); - - InstallationTokenResult installationTokenResult = - Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); - - assertWithMessage("Persisted Auth Token doesn't match") - .that(installationTokenResult.getToken()) - .isEqualTo(TEST_AUTH_TOKEN_2); - } - - @Test - public void testGetToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exception { - // Update local storage with a unregistered installation entry to validate that getToken - // calls getId to ensure FID registration and returns a valid auth token. - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - - InstallationTokenResult installationTokenResult = - Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); - - assertWithMessage("Persisted Auth Token doesn't match") - .that(installationTokenResult.getToken()) - .isEqualTo(TEST_AUTH_TOKEN); - } - - @Test - public void testGetAuthToken_authError_persistedInstallationCleared() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // Mocks error during auth token generation - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenReturn( - TokenResult.builder().setResponseCode(TokenResult.ResponseCode.AUTH_ERROR).build()); - - // Expect exception - try { - Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH)); - fail("the getAuthToken() call should have failed due to Auth Error."); - } catch (ExecutionException expected) { - assertWithMessage("Exception class doesn't match") - .that(expected) - .hasCauseThat() - .isInstanceOf(IOException.class); - } - - assertTrue(persistedInstallation.readPersistedInstallationEntryValue().isNotGenerated()); - } - - // /** - // * Check that a call to generateAuthToken(FORCE_REFRESH) fails if the backend client call - // * fails. - // */ - @Test - public void testGetAuthToken_serverError_failure() throws Exception { - // start the test with a registered FID - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // have the backend fail when generateAuthToken is invoked. - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenReturn( - TokenResult.builder().setResponseCode(TokenResult.ResponseCode.BAD_CONFIG).build()); - - // Make the forced getAuthToken call, which should fail. - try { - Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH)); - fail( - "getAuthToken() succeeded but should have failed due to the BAD_CONFIG error " - + "returned by the network call."); - } catch (ExecutionException expected) { - assertWithMessage("Exception class doesn't match") - .that(expected) - .hasCauseThat() - .isInstanceOf(FirebaseInstallationsException.class); - assertWithMessage("Exception status doesn't match") - .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(Status.BAD_CONFIG); - } - } - - @Test - public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce() - throws Exception { - // start with a valid fid and authtoken - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // Make the server generateAuthToken() call return a refreshed token - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_TOKEN_RESULT); - - // expire the authtoken by advancing the clock - fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); - - // Call getToken multiple times with DO_NOT_FORCE_REFRESH option - Task task1 = - firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); - Task task2 = - firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); - - Tasks.await(Tasks.whenAllComplete(task1, task2)); - - assertWithMessage("Persisted Auth Token doesn't match") - .that(task1.getResult().getToken()) - .isEqualTo(TEST_AUTH_TOKEN_2); - assertWithMessage("Persisted Auth Token doesn't match") - .that(task2.getResult().getToken()) - .isEqualTo(TEST_AUTH_TOKEN_2); - verify(mockBackend, times(1)) - .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); - } - - @Test - public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws Exception { - // start with a valid fid and authtoken - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // Use a mock ServiceClient for network calls with delay(500ms) to ensure first task is not - // completed before the second task starts. Hence, we can test multiple calls to getToken() - // and verify one task waits for another task to complete. - - doAnswer( - AdditionalAnswers.answersWithDelay( - 500, - (unused) -> - TokenResult.builder() - .setToken(TEST_AUTH_TOKEN_3) - .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setResponseCode(TokenResult.ResponseCode.OK) - .build())) - .doAnswer( - AdditionalAnswers.answersWithDelay( - 500, - (unused) -> - TokenResult.builder() - .setToken(TEST_AUTH_TOKEN_4) - .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setResponseCode(TokenResult.ResponseCode.OK) - .build())) - .when(mockBackend) - .generateAuthToken(anyString(), anyString(), anyString(), anyString()); - - // Call getToken multiple times with FORCE_REFRESH option. - Task task1 = - firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); - Task task2 = - firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); - Tasks.await(Tasks.whenAllComplete(task1, task2)); - - // As we cannot ensure which task got executed first, verifying with both expected values - assertWithMessage("Persisted Auth Token doesn't match") - .that(task1.getResult().getToken()) - .isEqualTo(TEST_AUTH_TOKEN_3); - assertWithMessage("Persisted Auth Token doesn't match") - .that(task2.getResult().getToken()) - .isEqualTo(TEST_AUTH_TOKEN_3); - verify(mockBackend, times(1)) - .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasAuthToken(TEST_AUTH_TOKEN_3); - } - - @Test - public void testDelete_registeredFID_successful() throws Exception { - // Update local storage with a registered installation entry - persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - - Tasks.await(firebaseInstallations.delete()); - - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertEquals(entryValue.getRegistrationStatus(), RegistrationStatus.NOT_GENERATED); - verify(mockBackend, times(1)) - .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); - } - - @Test - public void testDelete_unregisteredFID_successful() throws Exception { - // Update local storage with a unregistered installation entry - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - - Tasks.await(firebaseInstallations.delete()); - - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertEquals(entryValue.getRegistrationStatus(), RegistrationStatus.NOT_GENERATED); - verify(mockBackend, never()) - .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); - } - - @Test - public void testDelete_emptyPersistedFidEntry_successful() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withNoGeneratedFid()); - - Tasks.await(firebaseInstallations.delete()); - - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasRegistrationStatus(RegistrationStatus.NOT_GENERATED); - verify(mockBackend, never()) - .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); - } - - @Test - public void testDelete_serverError_badConfig() throws Exception { - // Update local storage with a registered installation entry - persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - - doThrow(new FirebaseException("Server Error")) - .when(mockBackend) - .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); - - // Expect exception - try { - Tasks.await(firebaseInstallations.delete()); - fail("firebaseInstallations.delete() failed due to Server Error."); - } catch (ExecutionException expected) { - assertWithMessage("Exception class doesn't match") - .that(expected) - .hasCauseThat() - .isInstanceOf(FirebaseInstallationsException.class); - assertWithMessage("Exception status doesn't match") - .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(Status.BAD_CONFIG); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).isEqualTo(REGISTERED_INSTALLATION_ENTRY); - } - } - - @Test - public void testDelete_networkError() throws Exception { - // Update local storage with a registered installation entry - persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - - doThrow(new IOException()) - .when(mockBackend) - .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); - - // Expect exception - try { - Tasks.await(firebaseInstallations.delete()); - fail("firebaseInstallations.delete() failed due to a Network Error."); - } catch (ExecutionException expected) { - assertWithMessage("Exception class doesn't match") - .that(expected) - .hasCauseThat() - .isInstanceOf(IOException.class); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).isEqualTo(REGISTERED_INSTALLATION_ENTRY); - } - } -} diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index 769ea8ec5a9..88b6dd62d4f 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -15,21 +15,11 @@ package com.google.firebase.installations; import com.google.firebase.installations.local.PersistedInstallationEntry; -import com.google.firebase.installations.remote.InstallationResponse; -import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; -import com.google.firebase.installations.remote.TokenResult; public final class FisAndroidTestConstants { public static final String TEST_FID_1 = "cccccccccccccccccccccc"; - public static final String TEST_PROJECT_ID = "777777777777"; - public static final String TEST_AUTH_TOKEN = "fis.auth.token"; - public static final String TEST_AUTH_TOKEN_2 = "fis.auth.token2"; - public static final String TEST_AUTH_TOKEN_3 = "fis.auth.token3"; - public static final String TEST_AUTH_TOKEN_4 = "fis.auth.token4"; - - public static final String TEST_API_KEY = "apiKey"; public static final String TEST_REFRESH_TOKEN = "1:test-refresh-token"; @@ -41,40 +31,6 @@ public final class FisAndroidTestConstants { public static final long TEST_CREATION_TIMESTAMP_1 = 2000L; public static final long TEST_CREATION_TIMESTAMP_2 = 2L; - public static final String TEST_INSTANCE_ID_1 = "ccccccccccc"; - public static final PersistedInstallationEntry DEFAULT_PERSISTED_INSTALLATION_ENTRY = PersistedInstallationEntry.builder().build(); - public static final InstallationResponse TEST_INSTALLATION_RESPONSE = - InstallationResponse.builder() - .setUri("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_FID_1) - .setFid(TEST_FID_1) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setAuthToken( - TokenResult.builder() - .setToken(TEST_AUTH_TOKEN) - .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .build()) - .setResponseCode(ResponseCode.OK) - .build(); - - public static final InstallationResponse TEST_INSTALLATION_RESPONSE_WITH_IID = - InstallationResponse.builder() - .setUri("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_INSTANCE_ID_1) - .setFid(TEST_INSTANCE_ID_1) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setAuthToken( - TokenResult.builder() - .setToken(TEST_AUTH_TOKEN) - .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .build()) - .setResponseCode(ResponseCode.OK) - .build(); - - public static final TokenResult TEST_TOKEN_RESULT = - TokenResult.builder() - .setToken(TEST_AUTH_TOKEN_2) - .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setResponseCode(TokenResult.ResponseCode.OK) - .build(); } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java index 702b4684d2a..d2b91e5a58d 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java @@ -45,7 +45,7 @@ public static PersistedInstallationEntrySubject assertThat( /** * Constructor for use by subclasses. If you want to create an instance of this class itself, call - * {@link Subject#check(String, PersistedInstallationEntry ..) check(...)}{@code .that(actual)}. + * {@link Subject#check(String, Object ...) check(...)}{@code .that(actual)}. * * @param metadata * @param actual diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 3dc316a7b8e..f6177fc4923 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -337,7 +337,7 @@ private PersistedInstallationEntry getPrefsWithGeneratedIdMultiProcessSafe() { } /** Use file locking to acquire a lock that will also block other processes. */ - private FileLock getCrossProcessLock() { + FileLock getCrossProcessLock() { try { File file = new File(firebaseApp.getApplicationContext().getFilesDir(), LOCKFILE_NAME_GENERATE_FID); @@ -351,7 +351,7 @@ private FileLock getCrossProcessLock() { } /** Release a previously acquired lock. */ - private void releaseCrossProcessLock(FileLock fileLock) { + void releaseCrossProcessLock(FileLock fileLock) { try { fileLock.release(); } catch (IOException e) { diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java b/firebase-installations/src/test/java/com/google/firebase/installations/FakeCalendar.java similarity index 98% rename from firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java rename to firebase-installations/src/test/java/com/google/firebase/installations/FakeCalendar.java index 3190d5911e8..536b479f876 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FakeCalendar.java @@ -23,10 +23,12 @@ public FakeCalendar(long initialTimeInMillis) { timeInMillis = initialTimeInMillis; } + @Override public long getTimeInMillis() { return timeInMillis; } + @Override public void setTimeInMillis(long timeInMillis) { this.timeInMillis = timeInMillis; } diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java index fe64dcf06b8..5dc10b77fda 100644 --- a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java @@ -14,5 +14,965 @@ package com.google.firebase.installations; +import static com.google.common.truth.Truth.assertWithMessage; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.matches; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import androidx.test.core.app.ApplicationProvider; +import com.google.android.gms.tasks.Task; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.installations.FirebaseInstallationsException.Status; +import com.google.firebase.installations.local.IidStore; +import com.google.firebase.installations.local.PersistedInstallation; +import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; +import com.google.firebase.installations.local.PersistedInstallationEntry; +import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; +import com.google.firebase.installations.remote.InstallationResponse; +import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; +import com.google.firebase.installations.remote.TokenResult; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + /** Tests for {@link FirebaseInstallations}. */ -public class FirebaseInstallationsTest {} +@RunWith(RobolectricTestRunner.class) +public class FirebaseInstallationsTest { + private FirebaseApp firebaseApp; + private ExecutorService executor; + private PersistedInstallation persistedInstallation; + @Mock private FirebaseInstallationServiceClient mockBackend; + @Mock private IidStore mockIidStore; + @Mock private RandomFidGenerator mockFidGenerator; + + public static final String TEST_FID_1 = "cccccccccccccccccccccc"; + + public static final String TEST_PROJECT_ID = "777777777777"; + + public static final String TEST_AUTH_TOKEN = "fis.auth.token"; + public static final String TEST_AUTH_TOKEN_2 = "fis.auth.token2"; + public static final String TEST_AUTH_TOKEN_3 = "fis.auth.token3"; + public static final String TEST_AUTH_TOKEN_4 = "fis.auth.token4"; + + public static final String TEST_API_KEY = "apiKey"; + + public static final String TEST_REFRESH_TOKEN = "1:test-refresh-token"; + + public static final String TEST_APP_ID_1 = "1:123456789:android:abcdef"; + + public static final long TEST_TOKEN_EXPIRATION_TIMESTAMP = 4000L; + + public static final String TEST_INSTANCE_ID_1 = "ccccccccccc"; + + public static final InstallationResponse TEST_INSTALLATION_RESPONSE = + InstallationResponse.builder() + .setUri("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_FID_1) + .setFid(TEST_FID_1) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setAuthToken( + TokenResult.builder() + .setToken(TEST_AUTH_TOKEN) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build()) + .setResponseCode(ResponseCode.OK) + .build(); + + public static final InstallationResponse TEST_INSTALLATION_RESPONSE_WITH_IID = + InstallationResponse.builder() + .setUri("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_INSTANCE_ID_1) + .setFid(TEST_INSTANCE_ID_1) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setAuthToken( + TokenResult.builder() + .setToken(TEST_AUTH_TOKEN) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build()) + .setResponseCode(ResponseCode.OK) + .build(); + + public static final TokenResult TEST_TOKEN_RESULT = + TokenResult.builder() + .setToken(TEST_AUTH_TOKEN_2) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setResponseCode(TokenResult.ResponseCode.OK) + .build(); + + private FirebaseInstallations firebaseInstallations; + private Utils utils; + private FakeCalendar fakeCalendar; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + FirebaseApp.clearInstancesForTest(); + executor = new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); + fakeCalendar = new FakeCalendar(5000000L); + firebaseApp = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder() + .setApplicationId(TEST_APP_ID_1) + .setProjectId(TEST_PROJECT_ID) + .setApiKey(TEST_API_KEY) + .build()); + persistedInstallation = new PersistedInstallation(firebaseApp); + persistedInstallation.clearForTesting(); + + utils = new Utils(fakeCalendar); + firebaseInstallations = + new FirebaseInstallations( + executor, + firebaseApp, + mockBackend, + persistedInstallation, + utils, + mockIidStore, + mockFidGenerator); + + when(mockFidGenerator.createRandomFid()).thenReturn(TEST_FID_1); + } + + @After + public void cleanUp() { + persistedInstallation.clearForTesting(); + try { + executor.awaitTermination(250, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + + } + } + + /** + * Check the id generation process when there is no network. There are three cases: + * + *
    + *
  • no iid -> generate a new fid + *
  • iid present -> make that iid into a fid + *
  • fid generated -> return that fid + *
+ */ + @Test + public void testGetId_noNetwork_noIid() throws Exception { + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockIidStore.readIid()).thenReturn(null); + + // Do the actual getId() call under test. Confirm that it returns a generated FID and + // and that the FID was written to storage. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_FID_1); + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid is registered. + entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + assertTrue("the entry isn't unregistered: " + entry, entry.isUnregistered()); + } + + @Test + public void testGetId_noNetwork_iidPresent() throws Exception { + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); + + // Do the actual getId() call under test. Confirm that it returns a generated FID and + // and that the FID was written to storage. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_INSTANCE_ID_1); + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_INSTANCE_ID_1)); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid is registered. + entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_INSTANCE_ID_1)); + assertTrue("the entry doesn't have an uregistered fid: " + entry, entry.isUnregistered()); + } + + @Test + public void testGetId_noNetwork_fidAlreadyGenerated() throws Exception { + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid("generatedFid")); + + // Do the actual getId() call under test. Confirm that it returns the already generated FID. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed.").that(fid).isEqualTo("generatedFid"); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid is registered. + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo("generatedFid")); + assertTrue("the entry doesn't have an uregistered fid: " + entry, entry.isUnregistered()); + } + + /** + * Checks that if we have a registered fid then the fid is returned and no backend calls are made. + */ + @Test + public void testGetId_ValidIdAndToken_NoBackendCalls() throws Exception { + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // No exception, means success. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_FID_1); + + // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second + // while we mock fid registration. We dont send an actual request to FIS in tests. + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // check that the mockClient didn't get invoked at all, since the fid is already registered + // and the authtoken is present and not expired + verifyZeroInteractions(mockBackend); + + // check that the fid is still the expected one and is registered + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + assertTrue("the entry doesn't have a registered fid: " + entry, entry.isRegistered()); + } + + /** + * Checks that if we have an unregistered fid that the fid gets registered with the backend and no + * other calls are made. + */ + @Test + public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception { + when(mockBackend.createFirebaseInstallation( + anyString(), matches(TEST_FID_1), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); + + // No exception, means success. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_FID_1); + + // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second + // while we mock fid registration. We dont send an actual request to FIS in tests. + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // check that the mockClient didn't get invoked at all, since the fid is already registered + // and the authtoken is present and not expired + verify(mockBackend) + .createFirebaseInstallation(anyString(), matches(TEST_FID_1), anyString(), anyString()); + verify(mockBackend, never()) + .generateAuthToken(anyString(), anyString(), anyString(), anyString()); + + // check that the fid is still the expected one and is registered + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + assertTrue("the entry doesn't have a registered fid: " + entry, entry.isRegistered()); + } + + @Test + public void testGetId_migrateIid_successful() throws Exception { + when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID); + + // Do the actual getId() call under test. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_INSTANCE_ID_1); + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_INSTANCE_ID_1)); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid si registered. + entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_INSTANCE_ID_1)); + assertTrue("the entry doesn't have a registered fid: " + entry, entry.isRegistered()); + } + + @Test + public void testGetId_multipleCalls_sameFIDReturned() throws Exception { + when(mockIidStore.readIid()).thenReturn(null); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + + // Call getId multiple times + Task task1 = firebaseInstallations.getId(); + Task task2 = firebaseInstallations.getId(); + TestOnCompleteListener onCompleteListener1 = new TestOnCompleteListener<>(); + task1.addOnCompleteListener(executor, onCompleteListener1); + TestOnCompleteListener onCompleteListener2 = new TestOnCompleteListener<>(); + task2.addOnCompleteListener(executor, onCompleteListener2); + onCompleteListener1.await(); + onCompleteListener2.await(); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + assertWithMessage("Persisted Fid of Task1 doesn't match.") + .that(task1.getResult()) + .isEqualTo(TEST_FID_1); + assertWithMessage("Persisted Fid of Task2 doesn't match.") + .that(task2.getResult()) + .isEqualTo(TEST_FID_1); + verify(mockBackend, times(1)) + .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + assertTrue("the entry isn't doesn't have a registered fid: " + entry, entry.isRegistered()); + } + + /** + * Checks that if the server rejects a FID during registration the SDK will use the fid in the + * response as the new fid. + */ + @Test + public void testGetId_unregistered_replacesFidWithResponse() throws Exception { + // Update local storage with installation entry that has invalid fid. + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid("tobereplaced")); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + + // The first call will return the existing FID, "tobereplaced" + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + + // do a getId(), the unregistered TEST_FID_1 should be returned + assertWithMessage("getId Task failed.").that(fid).isEqualTo("tobereplaced"); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The next call should return the FID that was returned by the server + onCompleteListener = new TestOnCompleteListener<>(); + task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + fid = onCompleteListener.await(); + + // do a getId(), the unregistered TEST_FID_1 should be returned + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_FID_1); + } + + /** + * A registration that fails with a SERVER_ERROR will cause the FID to be put into the error + * state. + */ + @Test + public void testGetId_ServerError_UnregisteredFID() throws Exception { + // start with an unregistered fid + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); + + // have the server return a server error for the registration + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn( + InstallationResponse.builder().setResponseCode(ResponseCode.BAD_CONFIG).build()); + + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + + // do a getId(), the unregistered TEST_FID_1 should be returned + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers. + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // We expect that the server error will cause the FID to be put into the error state. + // There is nothing more we can do. + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + // //assertThat(TEST_FID_1, eq(updatedInstallationEntry.getFirebaseInstallationId())); + // //assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); + } + + /** + * A registration that fails with an IOException will not cause the FID to be put into the error + * state. + */ + @Test + public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws Exception { + // set initial state to having an unregistered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); + + // Mocking unchecked exception on FIS createFirebaseInstallation + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task getIdTask = firebaseInstallations.getId(); + getIdTask.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + + assertEquals("fid doesn't match expected", TEST_FID_1, fid); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // We expect that the IOException will cause the request to fail, but it will not + // cause the FID to be put into the error state because we expect this to eventually succeed. + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + assertTrue("the entry doesn't have an unregistered fid: " + entry, entry.isUnregistered()); + } + + @Test + public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws Exception { + // Start with a registered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Move the time forward by the token expiration time. + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); + + // Mocking unchecked exception on FIS generateAuthToken + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task getIdTask = firebaseInstallations.getId(); + getIdTask.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + + assertWithMessage("getId Task failed").that(fid).isEqualTo(TEST_FID_1); + + // Waiting for Task that generates auth token with the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // Validate that registration status is still REGISTER + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + assertTrue("the entry doesn't have a registered fid: " + entry, entry.isRegistered()); + } + + /** + * The FID is successfully registered but the token is expired. A getId will cause the token to be + * refreshed in the background. + */ + @Test + public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { + // Start with a registered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Make the server generateAuthToken() call return a refreshed token + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_TOKEN_RESULT); + + // Move the time forward by the token expiration time. + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); + + // Get the ID, which should cause the SDK to realize that the auth token is expired and + // kick off a refresh of the token. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task getIdTask = firebaseInstallations.getId(); + getIdTask.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed").that(fid).isEqualTo(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + TestOnCompleteListener onCompleteListener2 = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener2); + InstallationTokenResult installationTokenResult = onCompleteListener2.await(); + + // Check that the token has been refreshed + assertWithMessage("auth token is not what is expected after the refresh") + .that(installationTokenResult.getToken()) + .isEqualTo(TEST_AUTH_TOKEN_2); + + verify(mockBackend, never()) + .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); + verify(mockBackend, times(1)) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } + + @Test + public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + + TestOnCompleteListener onCompleteListener = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getAuthToken(), equalTo(TEST_AUTH_TOKEN)); + } + + @Test + public void testGetAuthToken_fidExists_successful() throws Exception { + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + TestOnCompleteListener onCompleteListener = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener); + InstallationTokenResult installationTokenResult = onCompleteListener.await(); + + assertWithMessage("Persisted Auth Token doesn't match") + .that(installationTokenResult.getToken()) + .isEqualTo(TEST_AUTH_TOKEN); + verify(mockBackend, never()) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } + + @Test + public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { + // start with a registered FID and valid auth token + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Move the time forward by the token expiration time. + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); + + // have the server respond with a new token + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_TOKEN_RESULT); + + TestOnCompleteListener onCompleteListener = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener); + InstallationTokenResult installationTokenResult = onCompleteListener.await(); + + assertWithMessage("Persisted Auth Token doesn't match") + .that(installationTokenResult.getToken()) + .isEqualTo(TEST_AUTH_TOKEN_2); + } + + @Test + public void testGetToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exception { + // Update local storage with a unregistered installation entry to validate that getToken + // calls getId to ensure FID registration and returns a valid auth token. + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + + TestOnCompleteListener onCompleteListener = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener); + InstallationTokenResult installationTokenResult = onCompleteListener.await(); + + assertWithMessage("Persisted Auth Token doesn't match") + .that(installationTokenResult.getToken()) + .isEqualTo(TEST_AUTH_TOKEN); + } + + @Test + public void testGetAuthToken_authError_persistedInstallationCleared() throws Exception { + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Mocks error during auth token generation + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn( + TokenResult.builder().setResponseCode(TokenResult.ResponseCode.AUTH_ERROR).build()); + + // Expect exception + try { + TestOnCompleteListener onCompleteListener = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + fail("the getAuthToken() call should have failed due to Auth Error."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(IOException.class); + } + + assertTrue(persistedInstallation.readPersistedInstallationEntryValue().isNotGenerated()); + } + + // /** + // * Check that a call to generateAuthToken(FORCE_REFRESH) fails if the backend client call + // * fails. + // */ + @Test + public void testGetAuthToken_serverError_failure() throws Exception { + // start the test with a registered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // have the backend fail when generateAuthToken is invoked. + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn( + TokenResult.builder().setResponseCode(TokenResult.ResponseCode.BAD_CONFIG).build()); + + // Make the forced getAuthToken call, which should fail. + try { + TestOnCompleteListener onCompleteListener = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + fail( + "getAuthToken() succeeded but should have failed due to the BAD_CONFIG error " + + "returned by the network call."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(FirebaseInstallationsException.class); + assertWithMessage("Exception status doesn't match") + .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) + .isEqualTo(Status.BAD_CONFIG); + } + } + + @Test + public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce() + throws Exception { + // start with a valid fid and authtoken + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Make the server generateAuthToken() call return a refreshed token + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_TOKEN_RESULT); + + // expire the authtoken by advancing the clock + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); + + // Call getToken multiple times with DO_NOT_FORCE_REFRESH option + Task task1 = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + Task task2 = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + TestOnCompleteListener onCompleteListener1 = + new TestOnCompleteListener<>(); + task1.addOnCompleteListener(executor, onCompleteListener1); + TestOnCompleteListener onCompleteListener2 = + new TestOnCompleteListener<>(); + task2.addOnCompleteListener(executor, onCompleteListener2); + onCompleteListener1.await(); + onCompleteListener2.await(); + + assertWithMessage("Persisted Auth Token doesn't match") + .that(task1.getResult().getToken()) + .isEqualTo(TEST_AUTH_TOKEN_2); + assertWithMessage("Persisted Auth Token doesn't match") + .that(task2.getResult().getToken()) + .isEqualTo(TEST_AUTH_TOKEN_2); + verify(mockBackend, times(1)) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } + + @Test + @Ignore("the code doesn't currently enforce a single token fetch at a time") + public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws Exception { + // start with a valid fid and authtoken + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Use a mock ServiceClient for network calls with delay(500ms) to ensure first task is not + // completed before the second task starts. Hence, we can test multiple calls to getToken() + // and verify one task waits for another task to complete. + + doAnswer( + AdditionalAnswers.answersWithDelay( + 500, + (unused) -> + TokenResult.builder() + .setToken(TEST_AUTH_TOKEN_3) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setResponseCode(TokenResult.ResponseCode.OK) + .build())) + .doAnswer( + AdditionalAnswers.answersWithDelay( + 500, + (unused) -> + TokenResult.builder() + .setToken(TEST_AUTH_TOKEN_4) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setResponseCode(TokenResult.ResponseCode.OK) + .build())) + .when(mockBackend) + .generateAuthToken(anyString(), anyString(), anyString(), anyString()); + + // Call getToken multiple times with FORCE_REFRESH option. + // Call getToken multiple times with DO_NOT_FORCE_REFRESH option + Task task1 = + firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); + Task task2 = + firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); + TestOnCompleteListener onCompleteListener1 = + new TestOnCompleteListener<>(); + task1.addOnCompleteListener(executor, onCompleteListener1); + TestOnCompleteListener onCompleteListener2 = + new TestOnCompleteListener<>(); + task2.addOnCompleteListener(executor, onCompleteListener2); + onCompleteListener1.await(); + onCompleteListener2.await(); + + // As we cannot ensure which task got executed first, verifying with both expected values + assertWithMessage("Persisted Auth Token doesn't match") + .that(task1.getResult().getToken()) + .isEqualTo(TEST_AUTH_TOKEN_3); + assertWithMessage("Persisted Auth Token doesn't match") + .that(task2.getResult().getToken()) + .isEqualTo(TEST_AUTH_TOKEN_3); + verify(mockBackend, times(1)) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getAuthToken(), equalTo(TEST_AUTH_TOKEN_3)); + } + + @Test + public void testDelete_registeredFID_successful() throws Exception { + // Update local storage with a registered installation entry + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseInstallations.delete().addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertEquals(entryValue.getRegistrationStatus(), RegistrationStatus.NOT_GENERATED); + verify(mockBackend, times(1)) + .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } + + @Test + public void testDelete_unregisteredFID_successful() throws Exception { + // Update local storage with a unregistered installation entry + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); + + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseInstallations.delete().addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertEquals(entryValue.getRegistrationStatus(), RegistrationStatus.NOT_GENERATED); + verify(mockBackend, never()) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); + } + + @Test + public void testDelete_emptyPersistedFidEntry_successful() throws Exception { + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withNoGeneratedFid()); + + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseInstallations.delete().addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertTrue( + "the entry was expected to need a newly generated fid: " + entry, entry.isNotGenerated()); + + verify(mockBackend, never()) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); + } + + @Test + public void testDelete_serverError_badConfig() throws Exception { + // Update local storage with a registered installation entry + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + doThrow(new FirebaseException("Server Error")) + .when(mockBackend) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); + + // Expect exception + try { + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseInstallations.delete().addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + fail("firebaseInstallations.delete() failed due to Server Error."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(FirebaseInstallationsException.class); + assertWithMessage("Exception status doesn't match") + .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) + .isEqualTo(Status.BAD_CONFIG); + PersistedInstallationEntry entry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertTrue("the entry was expected to still be registered: " + entry, entry.isRegistered()); + } + } + + @Test + public void testDelete_networkError() throws Exception { + // Update local storage with a registered installation entry + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + doThrow(new IOException("simulated network error")) + .when(mockBackend) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); + + // Expect exception + try { + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseInstallations.delete().addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + fail("firebaseInstallations.delete() should have failed due to a Network Error."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(IOException.class); + PersistedInstallationEntry entry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertTrue( + "the entry was expected to still be registered since the delete failed: " + entry, + entry.isRegistered()); + } + } +} diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/TestOnCompleteListener.java b/firebase-installations/src/test/java/com/google/firebase/installations/TestOnCompleteListener.java new file mode 100644 index 00000000000..600c463e42b --- /dev/null +++ b/firebase-installations/src/test/java/com/google/firebase/installations/TestOnCompleteListener.java @@ -0,0 +1,70 @@ +// Copyright 2019 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.installations; + +import androidx.annotation.NonNull; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +/** + * Helper listener that works around a limitation of the Tasks API where await() cannot be called on + * the main thread. This listener works around it by running itself on a different thread, thus + * allowing the main thread to be woken up when the Tasks complete. + */ +public class TestOnCompleteListener implements OnCompleteListener { + private static final long TIMEOUT_MS = 5000; + private final CountDownLatch latch = new CountDownLatch(1); + private Task task; + private volatile TResult result; + private volatile Exception exception; + private volatile boolean successful; + + @Override + public void onComplete(@NonNull Task task) { + this.task = task; + successful = task.isSuccessful(); + if (successful) { + result = task.getResult(); + } else { + exception = task.getException(); + } + latch.countDown(); + } + + /** Blocks until the {@link #onComplete} is called. */ + public TResult await() throws InterruptedException, ExecutionException { + if (!latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + throw new InterruptedException("timed out waiting for result"); + } + if (successful) { + return result; + } else { + if (exception instanceof InterruptedException) { + throw (InterruptedException) exception; + } + if (exception instanceof FirebaseInstallationsException) { + throw new ExecutionException(exception); + } + if (exception instanceof IOException) { + throw new ExecutionException(exception); + } + throw new IllegalStateException("got an unexpected exception type", exception); + } + } +}