0
0
mirror of https://github.com/ankidroid/Anki-Android.git synced 2024-09-20 12:02:16 +02:00

Test for missing permission

Normally, this commit tests every cases which indirectly access the list of files of a
folder and which . I.e.
* Directory's hasFile
* MoveDirectoryContent.createInstance
* DeleteDirectory.execute
* MoveDirectory executeAll
This commit is contained in:
Arthur Milchior 2022-03-06 07:10:51 +01:00 committed by Mike Hardy
parent ad3c10bc3c
commit eb34dbb381
8 changed files with 159 additions and 43 deletions

View File

@ -16,14 +16,19 @@
package com.ichi2.anki.model
import android.os.Build
import androidx.annotation.RequiresApi
import com.ichi2.compat.Compat
import com.ichi2.compat.CompatHelper
import com.ichi2.compat.Test21And26
import com.ichi2.testutils.*
import com.ichi2.testutils.HamcrestUtils.containsInAnyOrder
import com.ichi2.testutils.assertThrows
import com.ichi2.testutils.withTempFile
import org.acra.util.IOUtils
import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.File
import java.io.FileNotFoundException
import kotlin.io.path.createTempDirectory
@ -32,7 +37,13 @@ import kotlin.io.path.pathString
/**
* Tests for [Directory]
*/
class DirectoryTest {
@RequiresApi(Build.VERSION_CODES.O) // This requirement is necessary for compilation. However, it still allows to test CompatV21
@RunWith(Parameterized::class)
class DirectoryTest(
override val compat: Compat,
/** Used in the "Test Results" Window */
@Suppress("unused") private val unitTestDescription: String
) : Test21And26(compat, unitTestDescription) {
@Test
fun passes_if_existing_directory() {
val path = createTempDirectory().pathString
@ -107,6 +118,17 @@ class DirectoryTest {
)
}
/**
* Reproduces https://github.com/ankidroid/Anki-Android/issues/10358
* Where for some reason, `listFiles` returned null on an existing directory and
* newDirectoryStream returned `AccessDeniedException`.
*/
@Test
fun reproduce_10358() {
val permissionDenied = createPermissionDenied(createTransientDirectory(), CompatHelper.getCompat())
permissionDenied.assertThrowsWhenPermissionDenied { permissionDenied.directory.hasFiles() }
}
private fun createValidTempDir(): Directory {
return Directory.createInstance(File(createTempDirectory().pathString))!!
}

View File

@ -16,11 +16,19 @@
package com.ichi2.anki.servicelayer.scopedstorage
import android.os.Build
import androidx.annotation.RequiresApi
import com.ichi2.anki.model.Directory
import com.ichi2.compat.Compat
import com.ichi2.compat.CompatHelper
import com.ichi2.compat.Test21And26
import com.ichi2.testutils.createTransientDirectory
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.hasSize
import org.hamcrest.Matchers.instanceOf
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.File
import kotlin.io.path.createTempDirectory
import kotlin.io.path.pathString
@ -28,7 +36,13 @@ import kotlin.io.path.pathString
/**
* Tests for [DeleteEmptyDirectory]
*/
class DeleteEmptyDirectoryTest : OperationTest {
@RequiresApi(Build.VERSION_CODES.O) // This requirement is necessary for compilation. However, it still allows to test CompatV21
@RunWith(Parameterized::class)
class DeleteEmptyDirectoryTest(
override val compat: Compat,
/** Used in the "Test Results" Window */
@Suppress("unused") private val unitTestDescription: String
) : Test21And26(compat, unitTestDescription), OperationTest {
override val executionContext = MockMigrationContext()
@ -68,6 +82,17 @@ class DeleteEmptyDirectoryTest : OperationTest {
assertThat("no more operations", next, hasSize(0))
}
/**
* Reproduces https://github.com/ankidroid/Anki-Android/issues/10358
* Where for some reason, `listFiles` returned null on an existing directory and
* newDirectoryStream returned `AccessDeniedException`.
*/
@Test
fun reproduce_10358() {
val permissionDenied = createPermissionDenied(createTransientDirectory(), CompatHelper.getCompat())
permissionDenied.assertThrowsWhenPermissionDenied { DeleteEmptyDirectory(permissionDenied.directory).execute(executionContext) }
}
private fun createEmptyDirectory() =
Directory.createInstance(File(createTempDirectory().pathString))!!
}

View File

@ -21,6 +21,7 @@ import androidx.annotation.RequiresApi
import com.ichi2.anki.model.Directory
import com.ichi2.anki.servicelayer.scopedstorage.MigrateUserData.Operation
import com.ichi2.compat.Compat
import com.ichi2.compat.CompatHelper
import com.ichi2.compat.Test21And26
import com.ichi2.testutils.*
import org.hamcrest.MatcherAssert.assertThat
@ -40,7 +41,7 @@ import java.nio.file.NotDirectoryException
/**
* Test for [MoveDirectoryContent]
*/
@RequiresApi(Build.VERSION_CODES.O) // ALlows code to compile, but we still test with [CompatV21]
@RequiresApi(Build.VERSION_CODES.O) // Allows code to compile, but we still test with [CompatV21]
@RunWith(Parameterized::class)
class MoveDirectoryContentTest(
override val compat: Compat,
@ -206,6 +207,17 @@ class MoveDirectoryContentTest(
}
}
/**
* Reproduces https://github.com/ankidroid/Anki-Android/issues/10358
* Where for some reason, `listFiles` returned null on an existing directory and
* newDirectoryStream returned `AccessDeniedException`.
*/
@Test
fun reproduce_10358() {
val permissionDenied = createPermissionDenied(createTransientDirectory(), CompatHelper.getCompat())
permissionDenied.assertThrowsWhenPermissionDenied { MoveDirectoryContent.createInstance(permissionDenied.directory, createTransientFile()) }
}
private fun moveDirectoryContent(source: Directory, destinationDirectory: File): MoveDirectoryContent {
return MoveDirectoryContent.createInstance(source, destinationDirectory)
}

View File

@ -16,14 +16,21 @@
package com.ichi2.anki.servicelayer.scopedstorage
import android.os.Build
import androidx.annotation.RequiresApi
import com.ichi2.anki.model.Directory
import com.ichi2.anki.servicelayer.scopedstorage.MigrateUserData.Operation
import com.ichi2.compat.Compat
import com.ichi2.compat.CompatHelper
import com.ichi2.compat.Test21And26
import com.ichi2.testutils.*
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.*
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.spy
@ -33,7 +40,13 @@ import java.io.File
/**
* Test for [MoveDirectory]
*/
class MoveDirectoryTest : OperationTest {
@RequiresApi(Build.VERSION_CODES.O) // This requirement is necessary for compilation. However, it still allows to test CompatV21
@RunWith(Parameterized::class)
class MoveDirectoryTest(
override val compat: Compat,
/** Used in the "Test Results" Window */
@Suppress("unused") private val unitTestDescription: String
) : Test21And26(compat, unitTestDescription), OperationTest {
override val executionContext: MockMigrationContext = MockMigrationContext()
@Test
@ -214,4 +227,16 @@ class MoveDirectoryTest : OperationTest {
assertThat("source was deleted", source.directory.exists(), equalTo(false))
}
/**
* Reproduces https://github.com/ankidroid/Anki-Android/issues/10358
* Where for some reason, `listFiles` returned null on an existing directory and
* newDirectoryStream returned `AccessDeniedException`.
*/
@Test
fun reproduce_10358() {
val sourceWithPermissionDenied = createPermissionDenied(createTransientDirectory(), CompatHelper.getCompat())
val destination = createTransientDirectory()
sourceWithPermissionDenied.assertThrowsWhenPermissionDenied { executeAll(MoveDirectory(sourceWithPermissionDenied.directory, destination)) }
}
}

View File

@ -16,6 +16,8 @@
package com.ichi2.compat;
import android.os.Build;
import com.ichi2.anki.TestUtils;
import org.junit.Assert;
@ -31,8 +33,11 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import androidx.annotation.RequiresApi;
import static com.ichi2.utils.FileOperation.getFileResource;
@RequiresApi(api = Build.VERSION_CODES.O) // This requirement is necessary for compilation. However, it still allows to test CompatV21
@RunWith(Parameterized.class)
public class CompatCopyFileTest extends Test21And26 {
public CompatCopyFileTest(Compat compat, String unitTestDescription) {

View File

@ -25,10 +25,6 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.mockito.kotlin.*
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.spy
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.nio.file.NotDirectoryException
@ -106,32 +102,6 @@ class CompatDirectoryContentTest(
}
}
/**
* Represents structure and compat required to simulate https://github.com/ankidroid/Anki-Android/issues/10358
* This is a bug that occurred in a smartphone, where listFiles returned `null` on an existing directory.
*/
class PermissionDenied private constructor(val directory: File, val compat: Compat) {
companion object {
fun createInstance(directory: File, compat: Compat): PermissionDenied {
val directoryWithPermissionDenied =
spy(directory) {
on { listFiles() } doReturn null
}
val compatWithPermissionDenied =
if (compat is CompatV26) {
// Closest to simulate [newDirectoryStream] throwing [AccessDeniedException]
// since this method calls toPath.
spy(compat) {
doThrow(AccessDeniedException(directory)).whenever(it).newDirectoryStream(eq(directory.toPath()))
}
} else {
compat
}
return PermissionDenied(directoryWithPermissionDenied, compatWithPermissionDenied)
}
}
}
/**
* Reproduces https://github.com/ankidroid/Anki-Android/issues/10358
* Where for some reason, `listFiles` returned null on an existing directory and
@ -139,7 +109,7 @@ class CompatDirectoryContentTest(
*/
@Test
fun reproduce_10358() {
val permissionDenied = PermissionDenied.createInstance(createTransientDirectory(), CompatHelper.getCompat())
assertThrowsSubclass<IOException> { permissionDenied.compat.contentOfDirectory(permissionDenied.directory) }
val permissionDenied = createPermissionDenied(createTransientDirectory(), CompatHelper.getCompat())
assertThrowsSubclass<IOException> { permissionDenied.compat.contentOfDirectory(permissionDenied.directory.directory) }
}
}

View File

@ -34,7 +34,7 @@ import java.nio.file.NotDirectoryException
/** Tests for [Compat.hasFiles] */
@RunWith(Parameterized::class)
@RequiresApi(Build.VERSION_CODES.O) // ALlows code to compile, but we still test with [CompatV21]
@RequiresApi(Build.VERSION_CODES.O) // Allows code to compile, but we still test with [CompatV21]
class CompatHasFilesTest(
override val compat: Compat,
/** Used in the "Test Results" Window */
@ -78,8 +78,8 @@ class CompatHasFilesTest(
*/
@Test
fun reproduce_10358() {
val permissionDenied = CompatDirectoryContentTest.PermissionDenied.createInstance(createTransientDirectory(), CompatHelper.getCompat())
assertThrowsSubclass<IOException> { permissionDenied.compat.hasFiles(permissionDenied.directory) }
val permissionDenied = createPermissionDenied(createTransientDirectory(), CompatHelper.getCompat())
assertThrowsSubclass<IOException> { permissionDenied.compat.hasFiles(permissionDenied.directory.directory) }
}
private fun hasFiles(dir: File) = compat.hasFiles(dir)

View File

@ -16,18 +16,25 @@
package com.ichi2.compat
import android.os.Build
import androidx.annotation.RequiresApi
import com.ichi2.anki.model.Directory
import com.ichi2.testutils.assertThrowsSubclass
import org.junit.After
import org.junit.Before
import org.junit.runners.Parameterized
import org.mockito.MockedStatic
import org.mockito.Mockito
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.*
import java.io.File
import java.io.IOException
/**
* Allows to test with CompatV21 and V26.
* In particular it allows to test version of the code that uses [Files] and [Path] classes.
* And versions that must restrict themselves to [File].
*/
@RequiresApi(Build.VERSION_CODES.O) // This requirement is necessary for compilation. However, it still allows to test CompatV21
open class Test21And26(
open val compat: Compat,
/** Used in the "Test Results" Window */
@ -56,8 +63,58 @@ open class Test21And26(
mocked.`when`<Compat> { CompatHelper.getCompat() }.doReturn(compat)
}
// Allow to cancel every static mock, appart from the setup's one.
// Required because individual method can't be unregistered.
fun restart() {
tearDown()
setup()
}
@After
fun tearDown() {
mocked.close()
}
/**
* Represents structure and compat required to simulate https://github.com/ankidroid/Anki-Android/issues/10358
* This is a bug that occurred in a smartphone, where listFiles returned `null` on an existing directory.
*/
inner class PermissionDenied constructor(val directory: Directory, val compat: Compat) {
/**
* This run test, ensuring that [newDirectoryStream] throws on [directory].
* This is useful in the case where we can't directly access the directory or compat
*/
fun <T> runWithPermissionDenied(test: () -> T): T {
mocked.`when`<Compat> { CompatHelper.getCompat() }.doReturn(compat)
val result = test()
restart()
return result
}
/**
* Allow to ensure that [test] throw an IOException.
* We plan to use it to ensure that if we don't have permission to read the directory
* the exception is not catched.
*/
fun assertThrowsWhenPermissionDenied(test: () -> Unit): IOException =
runWithPermissionDenied { assertThrowsSubclass(test) }
}
fun createPermissionDenied(directory: File, compat: Compat): PermissionDenied {
val directoryWithPermissionDenied =
spy(directory) {
on { listFiles() } doReturn null
}
val compatWithPermissionDenied =
if (compat is CompatV26) {
// Closest to simulate [newDirectoryStream] throwing [AccessDeniedException]
// since this method calls toPath.
spy(compat) {
doThrow(AccessDeniedException(directory)).whenever(it).newDirectoryStream(eq(directory.toPath()))
}
} else {
compat
}
return PermissionDenied(Directory.createInstance(directoryWithPermissionDenied)!!, compatWithPermissionDenied)
}
}