Skip to content

Commit

Permalink
Merge pull request #3711 from kiwix/Fix#3710
Browse files Browse the repository at this point in the history
Add test cases for getLocalFilePathByUri() method.
  • Loading branch information
kelson42 authored Mar 1, 2024
2 parents 0107edd + 7671a3b commit 98671f1
Show file tree
Hide file tree
Showing 3 changed files with 356 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,41 @@
package org.kiwix.kiwixmobile.utils.files

import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.test.platform.app.InstrumentationRegistry
import io.mockk.every
import io.mockk.mockk
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.Assertions
import org.kiwix.kiwixmobile.core.utils.files.FileUtils.getAllZimParts
import org.kiwix.kiwixmobile.core.utils.files.FileUtils.hasPart
import org.junit.jupiter.api.Assertions.assertEquals
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.core.utils.files.DocumentResolverWrapper
import org.kiwix.kiwixmobile.core.utils.files.FileUtils
import org.kiwix.kiwixmobile.core.utils.files.FileUtils.documentProviderContentQuery
import org.kiwix.kiwixmobile.core.utils.files.FileUtils.getAllZimParts
import org.kiwix.kiwixmobile.core.utils.files.FileUtils.hasPart
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.util.Random

class FileUtilsInstrumentationTest {
private var context: Context? = null
private var testDir: File? = null
private val commonPath = "Download/beer.stackexchange.com_en_all_2023-05.zim"
private val commonUri = "Download%2Fbeer.stackexchange.com_en_all_2023-05.zim"
private val downloadDocumentUriPrefix =
"content://com.android.providers.downloads.documents/document/"
private val primaryStorageUriPrefix =
"content://com.android.externalstorage.documents/document/"
private val downloadUriPrefix = "content://media/external/downloads/"
private val expectedFilePath = "${Environment.getExternalStorageDirectory()}/$commonPath"

@Before
fun executeBefore() {
Expand Down Expand Up @@ -246,5 +264,203 @@ class FileUtilsInstrumentationTest {
}
}

data class DummyUrlData(val url: String?, val expectedFileName: String?)
@Test
fun testGetLocalFilePathByUri() {
val loadFileStream =
FileUtilsInstrumentationTest::class.java.classLoader.getResourceAsStream("testzim.zim")
val zimFile = File(testDir, "testzim.zim")
if (zimFile.exists()) zimFile.delete()
zimFile.createNewFile()
loadFileStream.use { inputStream ->
val outputStream: OutputStream = FileOutputStream(zimFile)
outputStream.use { it ->
val buffer = ByteArray(inputStream.available())
var length: Int
while (inputStream.read(buffer).also { length = it } > 0) {
it.write(buffer, 0, length)
}
}
}
// get the SD card path
val sdCardPath = context?.getExternalFilesDirs("")
?.get(1)?.path?.substringBefore("/Android")
val dummyUriData = listOf(
// test the download uri on older devices
DummyUrlData(
null,
expectedFilePath,
Uri.parse("${downloadDocumentUriPrefix}raw%3A%2Fstorage%2Femulated%2F0%2F$commonUri")
),
// test the download uri with new version of android
DummyUrlData(
null,
expectedFilePath,
Uri.parse("$downloadDocumentUriPrefix%2Fstorage%2Femulated%2F0%2F$commonUri")
),
// test with file scheme
DummyUrlData(
null,
zimFile.path,
Uri.fromFile(zimFile)
),
// test with internal storage uri
DummyUrlData(
null,
expectedFilePath,
Uri.parse("${primaryStorageUriPrefix}primary%3A$commonUri")
),
// // test with SD card uri
DummyUrlData(
null,
"$sdCardPath/$commonPath",
Uri.parse(
primaryStorageUriPrefix +
sdCardPath?.substringAfter("storage/") +
"%3A$commonUri"
)
),
// test with USB stick uri
DummyUrlData(
null,
"/mnt/media_rw/USB/$commonPath",
Uri.parse("${primaryStorageUriPrefix}USB%3A$commonUri")
),
// test with invalid uri
DummyUrlData(
null,
null,
Uri.parse(primaryStorageUriPrefix)
),
// test with invalid download uri
DummyUrlData(
null,
null,
Uri.parse(
"${downloadUriPrefix}0"
)
)
)
context?.let { context ->
dummyUriData.forEach { dummyUrlData ->
dummyUrlData.uri?.let { uri ->
Assertions.assertEquals(
FileUtils.getLocalFilePathByUri(context, uri),
dummyUrlData.expectedFileName
)
}
}
}
}

@Test
fun testExtractDocumentId() {
val dummyDownloadUriData = arrayOf(
DummyUrlData(
null,
"raw:$expectedFilePath",
Uri.parse("${downloadDocumentUriPrefix}raw%3A%2Fstorage%2Femulated%2F0%2F$commonUri")
),
DummyUrlData(
null,
expectedFilePath,
Uri.parse("$downloadDocumentUriPrefix%2Fstorage%2Femulated%2F0%2F$commonUri")
),
DummyUrlData(
null,
"",
Uri.parse(downloadUriPrefix)
)
)

dummyDownloadUriData.forEach { dummyUrlData ->
dummyUrlData.uri?.let { uri ->
Assertions.assertEquals(
FileUtils.extractDocumentId(uri, DocumentResolverWrapper()),
dummyUrlData.expectedFileName
)
}
}

// Testing with a dynamically generated URI. This URI creates at runtime,
// and passing it statically would result in an `IllegalArgumentException` exception.
// Therefore, we simulate this scenario using the `DocumentsContractWrapper`
// to conduct the test.
val mockDocumentsContractWrapper: DocumentResolverWrapper = mockk()
val expectedDocumentId = "1000020403"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Below Android 10, download URI schemes are of the content type, such as:
// content://com.android.chrome.FileProvider/downloads/alpinelinux_en_all_nopic_2022-12.zim
// As a result, this method will not be called during that time. We are not testing it on
// Android versions below 10, as it would result in an "IllegalArgumentException" exception.
val mockedUri = Uri.parse("$downloadUriPrefix$expectedDocumentId")
every { mockDocumentsContractWrapper.getDocumentId(mockedUri) } returns expectedDocumentId
val actualDocumentId = FileUtils.extractDocumentId(mockedUri, mockDocumentsContractWrapper)
assertEquals(expectedDocumentId, actualDocumentId)
}
}

@Test
fun testDocumentProviderContentQuery() {
// test to get the download uri on old device
testWithDownloadUri(
Uri.parse("${downloadDocumentUriPrefix}raw%3A%2Fstorage%2Femulated%2F0%2F$commonUri"),
expectedFilePath
)

// test to get the download uri on new device
testWithDownloadUri(
Uri.parse("$downloadDocumentUriPrefix%2Fstorage%2Femulated%2F0%2F$commonUri"),
expectedFilePath
)

// test with all possible download uris
val contentUriPrefixes = arrayOf(
"content://downloads/public_downloads",
"content://downloads/my_downloads",
"content://downloads/all_downloads"
)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Below Android 10, download URI schemes are of the content type, such as:
// content://com.android.chrome.FileProvider/downloads/alpinelinux_en_all_nopic_2022-12.zim
// As a result, this method will not be called during that time. We are not testing it on
// Android versions below 10, as it would result in an "IllegalArgumentException" exception.
contentUriPrefixes.forEach {
val mockDocumentsContractWrapper: DocumentResolverWrapper = mockk()
val expectedDocumentId = "1000020403"
val mockedUri = Uri.parse("$it/$expectedDocumentId")
every { mockDocumentsContractWrapper.getDocumentId(mockedUri) } returns expectedDocumentId
every {
mockDocumentsContractWrapper.query(
context!!,
mockedUri,
"_data",
null,
null,
null
)
} returns expectedFilePath
testWithDownloadUri(
mockedUri,
expectedFilePath,
mockDocumentsContractWrapper
)
}
}
}

private fun testWithDownloadUri(
uri: Uri,
expectedPath: String,
documentsContractWrapper: DocumentResolverWrapper = DocumentResolverWrapper()
) {
context?.let { context ->
assertEquals(
expectedPath,
documentProviderContentQuery(context, uri, documentsContractWrapper)
)
}
}

data class DummyUrlData(val url: String?, val expectedFileName: String?, val uri: Uri? = null)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Kiwix Android
* Copyright (c) 2024 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package org.kiwix.kiwixmobile.core.utils.files

import android.content.Context
import android.net.Uri
import android.provider.DocumentsContract
import org.kiwix.kiwixmobile.core.extensions.get

/**
* Wrapper class created for `DocumentsContract` and `ContentResolver` methods.
* This class facilitates the usage of these methods in test cases where direct
* mocking is not feasible.
*/
class DocumentResolverWrapper {
fun getDocumentId(uri: Uri): String = DocumentsContract.getDocumentId(uri)

@Suppress("LongParameterList")
fun query(
context: Context,
uri: Uri,
columnName: String,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): String? = context.contentResolver.query(
uri,
arrayOf(columnName),
selection,
selectionArgs,
sortOrder
)?.use {
return@query if (it.moveToFirst() && it.getColumnIndex(columnName) != -1) {
it[columnName]
} else null
}
}
Loading

0 comments on commit 98671f1

Please sign in to comment.