Skip to content

getId() implementation with instrumented tests. #703

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Aug 30, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
30cf6ed
getId() implementation with instrumented tests.
ankitaj224 Aug 13, 2019
d034bbb
Addressing rayo's comments.
ankitaj224 Aug 14, 2019
f8b468c
Addressing rayo's comments.
ankitaj224 Aug 14, 2019
8db7967
Merge branch 'ankita_fis' of github.com:firebase/firebase-android-sdk…
ankitaj224 Aug 14, 2019
b507a3c
Merge branch 'create_fis' of github.com:firebase/firebase-android-sdk…
ankitaj224 Aug 14, 2019
f5a7c05
Merge branch 'ankita_fis' of github.com:firebase/firebase-android-sdk…
ankitaj224 Aug 14, 2019
90cdbaa
Addresing comments to resoleve the following:
ankitaj224 Aug 16, 2019
0dfa987
Addressing Ciaran and Rayo's comments.
ankitaj224 Aug 26, 2019
a71a1f3
Addressing Ciaran's comments
ankitaj224 Aug 27, 2019
0a92e2d
Addressing Ciaran's comments
ankitaj224 Aug 27, 2019
4cc0cee
Merge branch 'create_fis' of github.com:firebase/firebase-android-sdk…
ankitaj224 Aug 28, 2019
f16db3f
Merge branch 'ankita_fis' of github.com:firebase/firebase-android-sdk…
ankitaj224 Aug 29, 2019
f8e75fd
Adding param comments and checking if registration status is valid.
ankitaj224 Aug 30, 2019
227bf3b
Correcting lint warning: uses-permission should be declared before
ankitaj224 Aug 30, 2019
4be3847
Adding custom assertThat with more readable assertions
ankitaj224 Aug 30, 2019
68acde5
Correcting instrumented tests to be reliable.
ankitaj224 Aug 30, 2019
16544a6
Merge branch 'ankita_fis' of github.com:firebase/firebase-android-sdk…
ankitaj224 Aug 30, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions firebase-installations/firebase-installations.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,6 @@ dependencies {
androidTestImplementation "com.google.truth:truth:$googleTruthVersion"
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'
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,31 @@

package com.google.firebase.installations;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.runner.AndroidJUnit4;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.installations.local.FiidCache;
import com.google.firebase.installations.local.FiidCacheEntryValue;
import com.google.firebase.installations.remote.FirebaseInstallationServiceClient;
import com.google.firebase.installations.remote.FirebaseInstallationServiceException;
import com.google.firebase.installations.remote.InstallationResponse;
import java.util.concurrent.ExecutionException;
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.Mock;
import org.mockito.MockitoAnnotations;

/**
* Instrumented test, which will execute on an Android device.
Expand All @@ -26,4 +47,99 @@
*/
@RunWith(AndroidJUnit4.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class FirebaseInstallationsInstrumentedTest {}
public class FirebaseInstallationsInstrumentedTest {
private FirebaseApp firebaseApp;
@Mock private FirebaseInstallationServiceClient backendClientReturnsOk;
@Mock private FirebaseInstallationServiceClient backendClientReturnsError;
private FiidCache actualCache;
@Mock private FiidCache cacheReturnsError;

@Before
public void setUp() throws FirebaseInstallationServiceException {
MockitoAnnotations.initMocks(this);
FirebaseApp.clearInstancesForTest();
firebaseApp =
FirebaseApp.initializeApp(
ApplicationProvider.getApplicationContext(),
new FirebaseOptions.Builder()
.setApplicationId("1:123456789:android:abcdef")
.setProjectId("project-id")
.setApiKey("api_key")
.build());
actualCache = new FiidCache(firebaseApp);
when(backendClientReturnsOk.createFirebaseInstallation(
anyString(), anyString(), anyString(), anyString()))
.thenReturn(
InstallationResponse.builder()
.setName("/projects/123456789/installations/fid")
.setRefreshToken("refresh-token")
.setAuthToken(
InstallationTokenResult.builder()
.setToken("auth-token")
.setTokenExpirationTimestampMillis(1000L)
.build())
.build());
when(backendClientReturnsError.createFirebaseInstallation(
anyString(), anyString(), anyString(), anyString()))
.thenThrow(
new FirebaseInstallationServiceException(
"SDK Error", FirebaseInstallationServiceException.Status.SERVER_ERROR));
when(cacheReturnsError.insertOrUpdateCacheEntry(any())).thenReturn(false);
when(cacheReturnsError.readCacheEntryValue()).thenReturn(null);
}

@After
public void cleanUp() throws Exception {
actualCache.clear();
}

@Test
public void testCreateFirebaseInstallation_CacheOk_BackendOk() throws Exception {
FirebaseInstallations firebaseInstallations =
new FirebaseInstallations(firebaseApp, actualCache, backendClientReturnsOk);

// No exception, means success.
assertThat(Tasks.await(firebaseInstallations.getId())).isNotEmpty();
FiidCacheEntryValue entryValue = actualCache.readCacheEntryValue();
assertThat(entryValue.getFirebaseInstallationId()).isNotEmpty();
assertThat(entryValue.getCacheStatus()).isEqualTo(FiidCache.CacheStatus.REGISTERED);
}

@Test
public void testCreateFirebaseInstallation_CacheOk_BackendError() throws Exception {
FirebaseInstallations firebaseInstallations =
new FirebaseInstallations(firebaseApp, actualCache, backendClientReturnsError);

// Expect exception
try {
Tasks.await(firebaseInstallations.getId());
fail();
} catch (ExecutionException expected) {
Throwable cause = expected.getCause();
assertThat(cause).isInstanceOf(FirebaseInstallationsException.class);
assertThat(((FirebaseInstallationsException) cause).getStatus())
.isEqualTo(FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR);
}

FiidCacheEntryValue entryValue = actualCache.readCacheEntryValue();
assertThat(entryValue.getFirebaseInstallationId()).isNotEmpty();
assertThat(entryValue.getCacheStatus()).isEqualTo(FiidCache.CacheStatus.REGISTER_ERROR);
}

@Test
public void testCreateFirebaseInstallation_CacheError_BackendOk() throws InterruptedException {
FirebaseInstallations firebaseInstallations =
new FirebaseInstallations(firebaseApp, cacheReturnsError, backendClientReturnsOk);

// Expect exception
try {
Tasks.await(firebaseInstallations.getId());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of curiosity:
Does this framework not have "assertThrows"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isnt one. However, I found RC using one by adding Asssert in Test Utils.

https://github.com/firebase/firebase-android-sdk/blob/5301121df4727b357293b3b7de00555362f90170/firebase-config/src/test/java/com/google/firebase/remoteconfig/testutil/Assert.java

Rest of the firebase-android-sdk teams have handled the same way as me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fail();
} catch (ExecutionException expected) {
Throwable cause = expected.getCause();
assertThat(cause).isInstanceOf(FirebaseInstallationsException.class);
assertThat(((FirebaseInstallationsException) cause).getStatus())
.isEqualTo(FirebaseInstallationsException.Status.CLIENT_ERROR);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import com.google.android.gms.common.internal.Preconditions;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.installations.local.FiidCache;
import com.google.firebase.installations.local.FiidCacheEntryValue;
import com.google.firebase.installations.remote.FirebaseInstallationServiceClient;
import com.google.firebase.installations.remote.FirebaseInstallationServiceException;
import com.google.firebase.installations.remote.InstallationResponse;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
* Entry point for Firebase Installations.
Expand All @@ -35,10 +43,23 @@
public class FirebaseInstallations implements FirebaseInstallationsApi {

private final FirebaseApp firebaseApp;
private final FirebaseInstallationServiceClient serviceClient;
private final FiidCache localCache;
private final Executor executor;

/** package private constructor. */
FirebaseInstallations(FirebaseApp firebaseApp) {
this(firebaseApp, new FiidCache(firebaseApp), new FirebaseInstallationServiceClient());
}

FirebaseInstallations(
FirebaseApp firebaseApp,
FiidCache localCache,
FirebaseInstallationServiceClient serviceClient) {
this.firebaseApp = firebaseApp;
this.serviceClient = serviceClient;
this.executor = Executors.newFixedThreadPool(6);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not a constant?
Why 6?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it. Its a random number, based on 3 APIs I chose double the number of available threads.

this.localCache = localCache;
}

/**
Expand Down Expand Up @@ -71,7 +92,7 @@ public static FirebaseInstallations getInstance(@NonNull FirebaseApp app) {
@NonNull
@Override
public Task<String> getId() {
return Tasks.forResult("fid-is-better-than-iid");
return Tasks.call(executor, () -> createFirebaseInstallationId());
}

/** Returns a auth token(public key) of this Firebase app installation. */
Expand Down Expand Up @@ -103,4 +124,105 @@ String getApplicationId() {
String getName() {
return firebaseApp.getName();
}

/**
* Create firebase installation id for the {@link FirebaseApp} on FIS Servers and client side
* cache.
*
* <pre>
* The workflow is:
* check if cache empty or cache status is REGISTER_ERROR
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description is somewhat confusing to me: What happens if the status is REGISTER_ERROR or not?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also what happens if status is UNREGISTERED?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the description. Let me know, if you want me to change further.

* |
* create random fid
* |
* update cache with cache status UNREGISTERED
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a status "IN PROGRESS" or something to prevent multiple requests in parallel?
do you get a lock on the database entry or how do you deal with multithreading?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently don't have any IN PROGRESS state. I am looking at ios-sdk to see how its handled. I ll discuss with you in person about this.

* |
* send http request to backend
* | |
* on success: set cache entry status to REGISTERED |
* | |
* | on failure: set cache entry status to REGISTER_ERROR
* | |
* return throw exception
*
*
* else if cached FID exists
* | |
* if cache status is UNREGISTERED |
* | return cached FID
* send http request to backend
* | |
* on success: set cache \
* entry status to REGISTERED \
* | \
* | on failure: set cache entry status to REGISTER_ERROR
* | |
* return throw exception
* </pre>
*/
@WorkerThread
private String createFirebaseInstallationId() throws FirebaseInstallationsException {

FiidCacheEntryValue cacheEntryValue = localCache.readCacheEntryValue();

if (cacheEntryValue == null
|| cacheEntryValue.getCacheStatus() == FiidCache.CacheStatus.REGISTER_ERROR) {
String fid = Utils.createRandomFid();

boolean firstUpdateCacheResult =
localCache.insertOrUpdateCacheEntry(
FiidCacheEntryValue.create(fid, FiidCache.CacheStatus.UNREGISTERED, "", "", 0, 0));

if (!firstUpdateCacheResult) {
throw new FirebaseInstallationsException(
"Failed to update client side cache.",
FirebaseInstallationsException.Status.CLIENT_ERROR);
}

registerAndSaveFID(fid);

return fid;
} else if (!cacheEntryValue.getFirebaseInstallationId().isEmpty()) {

if (cacheEntryValue.getCacheStatus() == FiidCache.CacheStatus.UNREGISTERED) {
registerAndSaveFID(cacheEntryValue.getFirebaseInstallationId());
}

return cacheEntryValue.getFirebaseInstallationId();
}
return null;
}

/**
* Registers the created FID with FIS Servers if the Network is available and update the cache.
*/
private void registerAndSaveFID(String fid) throws FirebaseInstallationsException {
try {
if (Utils.isNetworkAvailable(firebaseApp.getApplicationContext())) {
long creationTime = Utils.getCurrentTimeInSeconds();

InstallationResponse installationResponse =
serviceClient.createFirebaseInstallation(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, I mentioned this before (I know it's pretty nitty):
Can we change the order of the parameters maybe?
like first FID, then API-Key, then all the metadata?
Or first the API-Key, then FID, then all the metadata?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it in the parent PR. 34848c6

firebaseApp.getOptions().getApiKey(),
firebaseApp.getOptions().getProjectId(),
fid,
getApplicationId());

localCache.insertOrUpdateCacheEntry(
FiidCacheEntryValue.create(
fid,
FiidCache.CacheStatus.REGISTERED,
installationResponse.getAuthToken().getToken(),
installationResponse.getRefreshToken(),
creationTime,
installationResponse.getAuthToken().getTokenExpirationTimestampMillis()));
}

} catch (FirebaseInstallationServiceException exception) {
localCache.insertOrUpdateCacheEntry(
FiidCacheEntryValue.create(fid, FiidCache.CacheStatus.REGISTER_ERROR, "", "", 0, 0));
throw new FirebaseInstallationsException(
exception.getMessage(), FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

package com.google.firebase.installations;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import androidx.annotation.NonNull;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
Expand Down Expand Up @@ -92,4 +95,15 @@ private static byte[] getBytesFromUUID(UUID uuid) {

return bb.array();
}

public static long getCurrentTimeInSeconds() {
return System.currentTimeMillis() / 1000;
}

public static boolean isNetworkAvailable(Context context) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above, I'd inject a "NetworkChecker" or similar so that you're not forced to muck around with a real ConnectivityManager in tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped the connectivity check here. Fred was of the opinion - the developer should check and handle the cases of network connectivity instead of us doing it.

ConnectivityManager connectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
return activeNetworkInfo != null && activeNetworkInfo.isConnected();
}
}