Firestore Test Production Security Rules With Android Unit Test

We shall test Firestore Security Rules in production environment using Android Unit Test (Instrumented tests).

NOTE: Other methods to test firestore security rules include Simulator and Firestore Emulator.

Android depedencies for testing.

dependencies {
    androidTestImplementation 'androidx.test:core:1.1.0'
    androidTestImplementation 'androidx.test:core-ktx:1.1.0'
    androidTestImplementation 'androidx.test.ext:junit:1.1.0'
    androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.0'
    androidTestImplementation 'androidx.test:runner:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

NOTE: Assuming you already setup Firestore on Android.

Create FirestoreSecurityRulesTest.kt at androidTest.

  • The following unit tests are to test against this User Security Rules.
  • Use @LargeTest because of network request (Firestore backend) is involved.
  • @FixMethodOrder(MethodSorters.NAME_ASCENDING) because some test involved creating document and query by another test.
  • expectFirestorePermissionDenied is used to detect rejected Firestore Security Rules.
  • Pre-create 3 users (2 normal users, 1 admin) using email & password authentication.
  • Assign admin role to one of the user.
  • Create methods to switch user such as signOut, signInAsUser1, signInAsUser2 and signInAsAdmin.
  • setup test by assigning db: FirebaseFirestore, auth: FirebaseAuth and user1: FirebaseUser.
  • We shall use user1 to create document, and use user1 (owner), user2 (non-owner) and admin (admin) to test access permission
  • tearUp is used to delete document created during this test

To run the test, right click on FirestoreSecurityRulesTest and select run 'FirestoreSecurityRulesTest'.

NOTE: When I run the test, occasionally network is lost/disabled on my device, thus I created test01_network to check for network issue. To solve this issue, launch the app (click the app icon on the device), then run the test again.

NOTE: This unit tests probably didn't conver all cases, but good enough as a starter.

import androidx.test.ext.junit.runners.AndroidJUnit4import androidx.test.filters.LargeTestimport com.google.android.gms.tasks.Tasksimport com.google.firebase.Timestampimport com.google.firebase.auth.FirebaseAuthimport com.google.firebase.auth.FirebaseUserimport com.google.firebase.firestore.FirebaseFirestoreimport com.google.firebase.firestore.FirebaseFirestoreExceptionimport com.google.firebase.firestore.FirebaseFirestoreSettingsimport org.hamcrest.CoreMatchers.*import org.hamcrest.MatcherAssert.assertThatimport org.hamcrest.Matchers.greaterThanimport org.hamcrest.core.StringContainsimport org.junit.*import org.junit.rules.ExpectedExceptionimport org.junit.runner.RunWithimport org.junit.runners.MethodSortersimport java.net.URLimport java.util.concurrent.ExecutionExceptionimport java.util.concurrent.TimeUnitimport javax.net.ssl.HttpsURLConnectionimport org.hamcrest.CoreMatchers.`is` as Is@RunWith(AndroidJUnit4::class)@LargeTest  // need network@FixMethodOrder(MethodSorters.NAME_ASCENDING)class FirestoreSecurityRulesTest {    companion object {        private const val COLLECTION_USER = "users"    }    private lateinit var db: FirebaseFirestore    private lateinit var auth: FirebaseAuth    private lateinit var user1: FirebaseUser    @Rule    @JvmField    val expectedException = ExpectedException.none()    fun expectFirestorePermissionDenied() {        expectedException.expect(ExecutionException::class.java)        expectedException.expectCause(isA(FirebaseFirestoreException::class.java))        expectedException.expectMessage(StringContains("PERMISSION_DENIED"))    }    fun signInAsUser1(): FirebaseUser {        auth.signOut()        Tasks.await(auth.signInWithEmailAndPassword("[email protected]", "***"))        assertThat(auth.currentUser, Is(notNullValue()))        assertThat(auth.currentUser?.email, Is("[email protected]"))        return auth.currentUser!!    }    fun signInAsUser2(): FirebaseUser {        auth.signOut()        Tasks.await(auth.signInWithEmailAndPassword("[email protected]", "***"))        assertThat(auth.currentUser, Is(notNullValue()))        assertThat(auth.currentUser?.email, Is("[email protected]"))        return auth.currentUser!!    }    fun signInAsAdmin(): FirebaseUser {        auth.signOut()        Tasks.await(auth.signInWithEmailAndPassword("[email protected]", "***"))        assertThat(auth.currentUser, Is(notNullValue()))        assertThat(auth.currentUser?.email, Is("[email protected]"))        return auth.currentUser!!    }    fun signOut() {        auth.signOut()        assertThat(auth.currentUser, Is(nullValue()))    }    @Before    fun setup() {        db = FirebaseFirestore.getInstance()        val settings = FirebaseFirestoreSettings.Builder()            .setPersistenceEnabled(false)            .build()        db.firestoreSettings = settings        auth = FirebaseAuth.getInstance()        user1 = signInAsUser1()    }    @After    fun tearUp() {        signInAsAdmin()        val batch = db.batch()        val collection = db.collection(COLLECTION_USER)        batch.delete(collection.document(user1.uid))        // these documents shouldn't be created        batch.delete(collection.document("test_create_user_public"))        batch.delete(collection.document("test_create_user_fake_id"))        batch.commit()    }    /**     * Sometimes network connection is disabled when running test     */    @Test    fun test01_network() {        val url = URL("https://www.android.com/")        val conn = url.openConnection() as HttpsURLConnection        assertThat(conn.responseCode, Is(HttpsURLConnection.HTTP_OK))    }    /**     * Public cannot create User document     */    @Test    fun test02_createUserByPublic() {        signOut()        val task = db.collection(COLLECTION_USER)            .document("test_create_user_public")            .set(mapOf("roles" to listOf("user")))        expectFirestorePermissionDenied()        Tasks.await(task, 10, TimeUnit.SECONDS)    }    /**     * User can create User document     */    @Test    fun test03_createUserAsUser1() {        val user = signInAsUser1()        val docRef = db.collection(COLLECTION_USER)            .document(user.uid)        val doc = Tasks.await(docRef.get())        // make sure it doesn't exist        assertThat(doc.exists(), Is(false))        val task = docRef            .set(mapOf(                "name" to "User1",                "roles" to listOf("user")            ))        Tasks.await(task, 10, TimeUnit.SECONDS)    }    /**     * User cannot create User with FakeId     */    @Test    fun test04_createUserWithFakeId() {        val user = signInAsUser1()        val docRef = db.collection(COLLECTION_USER)            .document("test_create_user_fake_id")        val doc = Tasks.await(docRef.get())        // make sure it doesn't exist        assertThat(doc.exists(), Is(false))        val task = docRef            .set(mapOf(                "name" to "User1",                "roles" to listOf("user")            ))        expectFirestorePermissionDenied()        Tasks.await(task, 10, TimeUnit.SECONDS)    }    /**     * User cannot create User document with admin role     */    @Test    fun test05_createUserWithAdminRole() {        val user = signInAsUser2()        val docRef = db.collection(COLLECTION_USER)            .document(user.uid)        val doc = Tasks.await(docRef.get())        // make sure it doesn't exist        assertThat(doc.exists(), Is(false))        val task = docRef            .set(mapOf(                "name" to "User2",                "roles" to listOf("user", "admin")            ))        expectFirestorePermissionDenied()        Tasks.await(task, 10, TimeUnit.SECONDS)    }    /**     * Public can read/list any User document     */    @Test    fun test06_readUserAsPublic() {        signOut()        val docRef = db.collection(COLLECTION_USER)            .document(user1.uid)        val doc = Tasks.await(docRef.get())        assertThat(doc.exists(), Is(true))        assertThat(doc.getString("name"), Is("User1"))        val task = db.collection(COLLECTION_USER)            .whereArrayContains("roles", "user")            .get()        val results = Tasks.await(task, 10, TimeUnit.SECONDS)        assertThat(results.size(), greaterThan(0))    }    /**     * Owner can update User document     */    @Test    fun test07_updateUserAsOwner() {        val user = signInAsUser1()        val docRef = db.collection(COLLECTION_USER)            .document(user.uid)        val doc = Tasks.await(docRef.get())        assertThat(doc.exists(), Is(true))        val task = docRef            .update(mapOf(                "modified" to Timestamp.now()            ))        Tasks.await(task, 10, TimeUnit.SECONDS)    }    /**     * Owner cannot change User document roles     */    @Test    fun test08_updateUserRolesAsOwner() {        val user = signInAsUser1()        val docRef = db.collection(COLLECTION_USER)            .document(user.uid)        val doc = Tasks.await(docRef.get())        assertThat(doc.exists(), Is(true))        val task = docRef            .update(mapOf(                "roles" to listOf("user", "admin")            ))        expectFirestorePermissionDenied()        Tasks.await(task, 10, TimeUnit.SECONDS)    }    /**     * Owner/User cannot delete User document     */    fun test09_deleteUserAsOwner() {        val user = signInAsUser1()        val docRef = db.collection(COLLECTION_USER)            .document(user.uid)        val doc = Tasks.await(docRef.get())        assertThat(doc.exists(), Is(true))        val task = docRef.delete()        expectFirestorePermissionDenied()        Tasks.await(task, 10, TimeUnit.SECONDS)    }    /**     * Non-owner cannot update User document     */    fun test10_updateUserAsNonOwner() {        val user = signInAsUser2()        val docRef = db.collection(COLLECTION_USER)            .document(user1.uid)        // make sure not owner        assertThat(docRef.id, Is(not(user.uid)))        val task = docRef.update(mapOf(            "name" to "cannot change name by non-owner"        ))        expectFirestorePermissionDenied()        Tasks.await(task, 10, TimeUnit.SECONDS)    }    /**     * Admin can do anything     */    fun test11_updateUserAsAdmin() {        val user = signInAsAdmin()        val docRef = db.collection(COLLECTION_USER)            .document(user1.uid)        // make sure not owner        assertThat(docRef.id, Is(not(user.uid)))        val task = docRef.update(mapOf(            "remark" to "changes by admin",            "roles" to listOf("user", "moderator")        ))    }}

❤️ Is this article helpful?

Buy me a coffee ☕ or support my work via PayPal to keep this space 🖖 and ad-free.

Do send some 💖 to @d_luaz or share this article.

✨ By Desmond Lua

A dream boy who enjoys making apps, travelling and making youtube videos. Follow me on @d_luaz

👶 Apps I built

Travelopy - discover travel places in Malaysia, Singapore, Taiwan, Japan.