Skip to content

Commit

Permalink
feat: Use local storage chosen by user as cache for images (#241)
Browse files Browse the repository at this point in the history
* feat: Use local storage chosen by user as cache for fullsize images

* refactor: rename FileDownloader

* feat: add cache for the thumbnails

* fix: imports in SignIn screen

* doc: complete documentation of DocumentsManager

* test: update tests for the refactoring made for the DocumentManager

* fix: add the ability to generate the thumbnail befaore downloading it

* test: fix the document upload e2e test

* test: fix unit tests

* test: remove broken assertion

* test: revert timeout for e2e test about document upload

* refactor: remove unused functions

* style: run ktfckFormat

* test: add tests for setSaveDocumentFolder

* test: check go back button on document list screen

* test: check that image document load

* test: disable sonar on AppModules

* test: fix test on setSaveDocumentFolder

* test: fix broken test on CI

* fix: regression for document upload

* fix: Missing document in the cache not loading anymore

* test: fix e2e tests with new fsUid and id for the example user
  • Loading branch information
ArthurChalard authored Dec 13, 2024
1 parent 96baa59 commit 6c85393
Show file tree
Hide file tree
Showing 28 changed files with 590 additions and 733 deletions.
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ sonar {
property("sonar.androidLint.reportPaths", "${project.layout.buildDirectory.get()}/reports/lint-results-debug.xml")
// Paths to JaCoCo XML coverage report files.
property("sonar.coverage.jacoco.xmlReportPaths", "${project.layout.buildDirectory.get()}/reports/jacoco/jacocoTestReport/jacocoTestReport.xml")

// Exclude the dependency injections from the coverage report
property("sonar.coverage.exclusions", "**/di/AppModules.kt, ")
}
}

Expand All @@ -193,6 +196,7 @@ dependencies {
implementation(libs.androidx.runtime.livedata)
implementation(libs.androidx.navigation.testing)
implementation(libs.play.services.location)
implementation(libs.androidx.datastore.preferences)
testImplementation(libs.junit)
globalTestImplementation(libs.androidx.junit)
globalTestImplementation(libs.androidx.espresso.core)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package com.github.se.travelpouch.di

import android.content.Context
import com.github.se.travelpouch.helper.FileDownloader
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStoreFile
import com.github.se.travelpouch.R
import com.github.se.travelpouch.model.activity.ActivityRepository
import com.github.se.travelpouch.model.activity.ActivityRepositoryFirebase
import com.github.se.travelpouch.model.authentication.AuthenticationService
import com.github.se.travelpouch.model.authentication.FirebaseAuthenticationService
import com.github.se.travelpouch.model.documents.DocumentRepository
import com.github.se.travelpouch.model.documents.DocumentRepositoryFirestore
import com.github.se.travelpouch.model.documents.DocumentsManager
import com.github.se.travelpouch.model.events.EventRepository
import com.github.se.travelpouch.model.events.EventRepositoryFirebase
import com.github.se.travelpouch.model.notifications.NotificationRepository
Expand Down Expand Up @@ -108,9 +113,16 @@ object TestModule {
@Singleton
fun provideFileDownloader(
@ApplicationContext context: Context,
storage: FirebaseStorage
): FileDownloader {
return FileDownloader(context.contentResolver, storage)
storage: FirebaseStorage,
functions: FirebaseFunctions,
dataStore: DataStore<Preferences>
): DocumentsManager {
return DocumentsManager(
context.contentResolver,
storage,
functions,
dataStore,
context.getDir(context.getString(R.string.thumbs_dir_name), Context.MODE_PRIVATE))
}

@Provides
Expand All @@ -130,4 +142,11 @@ object TestModule {
fun provideTravelRepository(db: FirebaseFirestore): TravelRepository {
return TravelRepositoryFirestore(db)
}

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Preferences> {
return PreferenceDataStoreFactory.create(
produceFile = { context.preferencesDataStoreFile("documents") })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ private const val DEFAULT_TIMEOUT = 10000L
@HiltAndroidTest
@UninstallModules(AppModule::class)
class DocumentUpload {
lateinit var file: File
private lateinit var file: File

@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)

Expand Down Expand Up @@ -110,12 +110,11 @@ class DocumentUpload {
auth.signOut()
}

val context = getInstrumentation().context
file = File.createTempFile("mountain", ".png")
getInstrumentation()
.context
.resources
.openRawResource(com.github.se.travelpouch.test.R.drawable.mountain)
.use { file.outputStream().use { output -> it.copyTo(output) } }
context.resources.openRawResource(com.github.se.travelpouch.test.R.drawable.mountain).use {
file.outputStream().use { output -> it.copyTo(output) }
}
}

@After
Expand Down Expand Up @@ -148,6 +147,9 @@ class DocumentUpload {
.respondWith(
Instrumentation.ActivityResult(
Activity.RESULT_OK, Intent().setData(Uri.fromFile(file))))
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT_TREE))
.respondWith(
Instrumentation.ActivityResult(Activity.RESULT_OK, Intent().setData(Uri.EMPTY)))

// assert that login screen is displayed
composeTestRule.onNodeWithTag("appLogo").assertIsDisplayed()
Expand Down Expand Up @@ -214,10 +216,27 @@ class DocumentUpload {
composeTestRule.waitUntil(timeoutMillis = 5000) {
composeTestRule.onNodeWithTag("documentListItem", useUnmergedTree = true).isDisplayed()
}

val newDocumentRefId =
firestore
.collection("allTravels/skibidiJ4KgcXJ5nVxkF/documents")
.get()
.await()
.documents
.first()
.reference
.id

composeTestRule.waitUntil(timeoutMillis = DEFAULT_TIMEOUT) {
composeTestRule
.onNodeWithTag("thumbnail-$newDocumentRefId", useUnmergedTree = true)
.isDisplayed()
}
composeTestRule.onNodeWithTag("documentListItem").assertIsDisplayed()
composeTestRule.onNodeWithTag("documentListItem").performClick()

composeTestRule.waitUntil(timeoutMillis = 5000) {
composeTestRule.onNodeWithTag("document").isDisplayed()
composeTestRule.waitUntil(timeoutMillis = DEFAULT_TIMEOUT) {
composeTestRule.onNodeWithTag("documentPreviewScreen").isDisplayed()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,6 @@ class TravelCreation {
@Inject lateinit var firestore: FirebaseFirestore
@Inject lateinit var auth: FirebaseAuth

// @Before
// fun setUp() {
// hiltRule.inject()
// }

@Before
fun setUp() {
hiltRule.inject()
Expand Down Expand Up @@ -87,7 +82,15 @@ class TravelCreation {

@After
fun tearDown() {
runBlocking { firestore.terminate().await() }
runBlocking {
firestore.terminate().await()
auth.signOut()
auth
.signInWithEmailAndPassword("[email protected]", "password1")
.await()
val uid = auth.currentUser!!.uid
auth.currentUser!!.delete().await()
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,61 +1,71 @@
package com.github.se.travelpouch.helper
package com.github.se.travelpouch.model.documents

import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.preferencesOf
import androidx.documentfile.provider.DocumentFile
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.google.firebase.FirebaseApp
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import com.google.firebase.storage.FirebaseStorage
import java.nio.file.Files
import kotlin.random.Random
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.tasks.await
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.ArgumentMatchers
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.never

@RunWith(MockitoJUnitRunner::class)
class FileDownloaderTest {
class DocumentsManagerTest {

private lateinit var contentResolver: ContentResolver
private lateinit var mockDataStore: DataStore<Preferences>
private lateinit var mockDocumentFile: DocumentFile
private lateinit var mockUri: Uri
private lateinit var mockDestinationFolder: DocumentFile
private lateinit var mockFirebaseStorage: FirebaseStorage

@Before
fun setUp() {
mockDocumentFile = mock(DocumentFile::class.java)
mockUri = mock(Uri::class.java)
mockDestinationFolder = mock(DocumentFile::class.java)
mockFirebaseStorage = mock(FirebaseStorage::class.java)
mockDocumentFile = Mockito.mock(DocumentFile::class.java)
mockUri = Mockito.mock(Uri::class.java)
mockDestinationFolder = Mockito.mock(DocumentFile::class.java)
mockFirebaseStorage = Mockito.mock(FirebaseStorage::class.java)
val context = ApplicationProvider.getApplicationContext<Context>()
contentResolver = context.contentResolver
mockDataStore = mock()
FirebaseApp.initializeApp(context)
}

@Test
fun assertUnableToCreateFile() {
val fileDownloader = FileDownloader(contentResolver, mockFirebaseStorage)
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.executeShellCommand("logcat -c")
`when`(mockDestinationFolder.createFile(any(), any())).thenReturn(null)
fileDownloader.downloadFile(
"image/jpeg", "mountain.jpg", "hWwbmtbnfwX5yRhAwL3o", mockDestinationFolder)
verify(mockFirebaseStorage, never()).reference
val logs = device.executeShellCommand("logcat -d")
assert(logs.contains("Failed to create document file in specified directory"))

`when`(mockDataStore.data).thenReturn(flowOf(preferencesOf()))
val documentsManager =
DocumentsManager(contentResolver, mockFirebaseStorage, mock(), mockDataStore, mock())
`when`(mockDestinationFolder.createFile(ArgumentMatchers.any(), ArgumentMatchers.any()))
.thenReturn(null)
documentsManager
.getDocument("image/jpeg", "mountain.jpg", "hWwbmtbnfwX5yRhAwL3o", mockDestinationFolder)
.invokeOnCompletion {
assert(it is Exception) { "Expected an exception but got $it" }
assertEquals(it?.message, "Failed to create document file in specified directory")
Mockito.verify(mockFirebaseStorage, never()).reference
}
}

@Test
Expand All @@ -66,8 +76,10 @@ class FileDownloaderTest {
val tempFile = Files.createTempFile("testDownload", ".tmp").toFile()
val uri = Uri.fromFile(tempFile)

`when`(mockDestinationFolder.createFile(any(), any())).thenReturn(mockDocumentFile)
`when`(mockDestinationFolder.createFile(ArgumentMatchers.any(), ArgumentMatchers.any()))
.thenReturn(mockDocumentFile)
`when`(mockDocumentFile.uri).thenReturn(uri)
`when`(mockDataStore.data).thenReturn(flowOf(preferencesOf()))

val storage =
FirebaseStorage.getInstance("gs://travelpouch-7d692.appspot.com").apply {
Expand All @@ -78,13 +90,13 @@ class FileDownloaderTest {
val auth = Firebase.auth.apply { useEmulator("10.0.2.2", 9099) }

runBlocking {
val taskSnapshot = storage.reference.child("hWwbmtbnfwX5yRhAwL3o").putBytes(data)
auth.signInAnonymously().await()
val taskSnapshot = storage.reference.child("hWwbmtbnfwX5yRhAwL3o").putBytes(data)
taskSnapshot.await()
taskSnapshot.result

FileDownloader(contentResolver, storage)
.downloadFile("image/jpeg", "mountain.jpg", "hWwbmtbnfwX5yRhAwL3o", mockDestinationFolder)
DocumentsManager(contentResolver, storage, mock(), mockDataStore, mock())
.getDocument("image/jpeg", "mountain.jpg", "hWwbmtbnfwX5yRhAwL3o", mockDestinationFolder)
.join()
}
val downloadedData = Files.readAllBytes(tempFile.toPath())
Expand Down
Loading

0 comments on commit 6c85393

Please sign in to comment.