We shall test Firestore Security Rules in production environment using Android Unit Test (Instrumented tests).
- We will be using Email & Password authentication
- Need to pre-create users
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
andsignInAsAdmin
. setup
test by assigningdb: FirebaseFirestore
,auth: FirebaseAuth
anduser1: FirebaseUser
.- We shall use
user1
to create document, and useuser1
(owner),user2
(non-owner) andadmin
(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") )) }}