Add instrumentation tests for storage plugin (SAF)

This commit is contained in:
Torsten Grote 2020-08-06 13:49:33 -03:00 committed by Chirayu Desai
parent 2f62e9515c
commit 22aaaeb1fd
8 changed files with 272 additions and 4 deletions

View file

@ -2,7 +2,7 @@ package com.stevesoltys.seedvault
import androidx.documentfile.provider.DocumentFile
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
import com.stevesoltys.seedvault.plugins.saf.createOrGetFile

View file

@ -0,0 +1,252 @@
package com.stevesoltys.seedvault
import androidx.test.core.content.pm.PackageInfoBuilder
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderBackupPlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderRestorePlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
import com.stevesoltys.seedvault.plugins.saf.deleteContents
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
import io.mockk.every
import io.mockk.mockk
import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.KoinComponent
import org.koin.core.inject
import java.io.InputStream
import java.io.OutputStream
import kotlin.random.Random
@RunWith(AndroidJUnit4::class)
class PluginTest : KoinComponent {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val metadataManager: MetadataManager by inject()
private val settingsManager: SettingsManager by inject()
private val mockedSettingsManager: SettingsManager = mockk()
private val storage = DocumentsStorage(context, metadataManager, mockedSettingsManager)
private val backupPlugin: BackupPlugin = DocumentsProviderBackupPlugin(storage, context.packageManager)
private val restorePlugin: RestorePlugin = DocumentsProviderRestorePlugin(context, storage)
private val token = Random.nextLong()
private val packageInfo = PackageInfoBuilder.newBuilder().setPackageName("org.example").build()
private val packageInfo2 = PackageInfoBuilder.newBuilder().setPackageName("net.example").build()
@Before
fun setup() {
every { mockedSettingsManager.getStorage() } returns settingsManager.getStorage()
storage.rootBackupDir?.deleteContents()
?: error("Select a storage location in the app first!")
}
@After
fun tearDown() {
storage.rootBackupDir?.deleteContents()
}
@Test
fun testProviderPackageName() {
assertNotNull(backupPlugin.providerPackageName)
}
@Test
fun testMetadataWriteRead() {
every { mockedSettingsManager.getAndResetIsStorageChanging() } returns true andThen false
assertTrue(backupPlugin.initializeDevice(newToken = token))
// write metadata
val metadata = getRandomByteArray()
backupPlugin.getMetadataOutputStream().writeAndClose(metadata)
// get available backups, expect only one with our token and no error
var availableBackups = restorePlugin.getAvailableBackups()?.toList()
check(availableBackups != null)
assertEquals(1, availableBackups.size)
assertEquals(token, availableBackups[0].token)
assertFalse(availableBackups[0].error)
// read metadata matches what was written earlier
assertEquals(metadata, availableBackups[0].inputStream)
// initializing again (without changing storage) keeps restore set with same token
assertFalse(backupPlugin.initializeDevice(newToken = token + 1))
availableBackups = restorePlugin.getAvailableBackups()?.toList()
check(availableBackups != null)
assertEquals(1, availableBackups.size)
assertEquals(token, availableBackups[0].token)
assertFalse(availableBackups[0].error)
// metadata hasn't changed
assertEquals(metadata, availableBackups[0].inputStream)
}
@Test
fun testApkWriteRead() {
// initialize storage with given token
initStorage(token)
// write random bytes as APK
val apk = getRandomByteArray(1337)
backupPlugin.getApkOutputStream(packageInfo).writeAndClose(apk)
// assert that read APK bytes match what was written
assertEquals(apk, restorePlugin.getApkInputStream(token, packageInfo.packageName))
}
@Test
fun testKvBackupRestore() {
// define shortcuts
val kvBackup = backupPlugin.kvBackupPlugin
val kvRestore = restorePlugin.kvRestorePlugin
// initialize storage with given token
initStorage(token)
// no data available for given package
assertFalse(kvBackup.hasDataForPackage(packageInfo))
assertFalse(kvRestore.hasDataForPackage(token, packageInfo))
// define key/value pair records
val record1 = Pair(getRandomBase64(23), getRandomByteArray(1337))
val record2 = Pair(getRandomBase64(42), getRandomByteArray(42 * 1024))
val record3 = Pair(getRandomBase64(255), getRandomByteArray(5 * 1024 * 1024))
// write first record
kvBackup.ensureRecordStorageForPackage(packageInfo)
kvBackup.getOutputStreamForRecord(packageInfo, record1.first).writeAndClose(record1.second)
// data is now available for current token and given package, but not for different token
assertTrue(kvBackup.hasDataForPackage(packageInfo))
assertTrue(kvRestore.hasDataForPackage(token, packageInfo))
assertFalse(kvRestore.hasDataForPackage(token + 1, packageInfo))
// record for package is found and returned properly
var records = kvRestore.listRecords(token, packageInfo)
assertEquals(1, records.size)
assertEquals(record1.first, records[0])
assertEquals(record1.second, kvRestore.getInputStreamForRecord(token, packageInfo, record1.first))
// write second and third record
kvBackup.ensureRecordStorageForPackage(packageInfo)
kvBackup.getOutputStreamForRecord(packageInfo, record2.first).writeAndClose(record2.second)
kvBackup.getOutputStreamForRecord(packageInfo, record3.first).writeAndClose(record3.second)
// all records for package are found and returned properly
records = kvRestore.listRecords(token, packageInfo)
assertEquals(listOf(record1.first, record2.first, record3.first).sorted(), records.sorted())
assertEquals(record1.second, kvRestore.getInputStreamForRecord(token, packageInfo, record1.first))
assertEquals(record2.second, kvRestore.getInputStreamForRecord(token, packageInfo, record2.first))
assertEquals(record3.second, kvRestore.getInputStreamForRecord(token, packageInfo, record3.first))
// delete record3 and ensure that the other two are still found
kvBackup.deleteRecord(packageInfo, record3.first)
records = kvRestore.listRecords(token, packageInfo)
assertEquals(listOf(record1.first, record2.first).sorted(), records.sorted())
// remove all data of package and ensure that it is gone
kvBackup.removeDataOfPackage(packageInfo)
assertFalse(kvBackup.hasDataForPackage(packageInfo))
assertFalse(kvRestore.hasDataForPackage(token, packageInfo))
}
@Test
fun testMaxKvKeyLength() {
// define shortcuts
val kvBackup = backupPlugin.kvBackupPlugin
val kvRestore = restorePlugin.kvRestorePlugin
// initialize storage with given token
initStorage(token)
// define record with maximum key length and one above the maximum
val recordMax = Pair(getRandomBase64(255), getRandomByteArray(1024))
val recordOver = Pair(getRandomBase64(256), getRandomByteArray(1024))
// write max record
kvBackup.ensureRecordStorageForPackage(packageInfo)
kvBackup.getOutputStreamForRecord(packageInfo, recordMax.first).writeAndClose(recordMax.second)
// max record is found correctly
assertTrue(kvRestore.hasDataForPackage(token, packageInfo))
var records = kvRestore.listRecords(token, packageInfo)
assertEquals(listOf(recordMax.first), records)
// write exceeding key length record
kvBackup.ensureRecordStorageForPackage(packageInfo)
kvBackup.getOutputStreamForRecord(packageInfo, recordOver.first).writeAndClose(recordOver.second)
// exceeding record gets truncated
assertTrue(kvRestore.hasDataForPackage(token, packageInfo))
records = kvRestore.listRecords(token, packageInfo)
assertNotEquals(listOf(recordMax.first, recordOver.first).sorted(), records.sorted())
}
@Test
fun testFullBackupRestore() {
// define shortcuts
val fullBackup = backupPlugin.fullBackupPlugin
val fullRestore = restorePlugin.fullRestorePlugin
// initialize storage with given token
initStorage(token)
// no data available initially
assertFalse(fullRestore.hasDataForPackage(token, packageInfo))
assertFalse(fullRestore.hasDataForPackage(token, packageInfo2))
// write full backup data
val data = getRandomByteArray(5 * 1024 * 1024)
fullBackup.getOutputStream(packageInfo).writeAndClose(data)
// data is available now, but only this token
assertTrue(fullRestore.hasDataForPackage(token, packageInfo))
assertFalse(fullRestore.hasDataForPackage(token + 1, packageInfo))
// restore data matches backed up data
assertEquals(data, fullRestore.getInputStreamForPackage(token, packageInfo))
// write and check data for second package
val data2 = getRandomByteArray(5 * 1024 * 1024)
fullBackup.getOutputStream(packageInfo2).writeAndClose(data2)
assertTrue(fullRestore.hasDataForPackage(token, packageInfo2))
assertEquals(data2, fullRestore.getInputStreamForPackage(token, packageInfo2))
// remove data of first package again and ensure that no more data is found
fullBackup.removeDataOfPackage(packageInfo)
assertFalse(fullRestore.hasDataForPackage(token, packageInfo))
// second package is still there
assertTrue(fullRestore.hasDataForPackage(token, packageInfo2))
// ensure that it gets deleted as well
fullBackup.removeDataOfPackage(packageInfo2)
assertFalse(fullRestore.hasDataForPackage(token, packageInfo2))
}
private fun initStorage(token: Long) {
every { mockedSettingsManager.getAndResetIsStorageChanging() } returns true
assertTrue(backupPlugin.initializeDevice(newToken = token))
}
private fun OutputStream.writeAndClose(data: ByteArray) = use {
it.write(data)
}
private fun assertEquals(data: ByteArray, inputStream: InputStream?) = inputStream?.use {
assertArrayEquals(data, it.readBytes())
} ?: error("no input stream")
}

View file

@ -10,7 +10,7 @@ internal class DocumentsProviderFullRestorePlugin(
@Throws(IOException::class)
override fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean {
val backupDir = documentsStorage.getFullBackupDir(token) ?: throw IOException()
val backupDir = documentsStorage.getFullBackupDir(token) ?: return false
return backupDir.findFile(packageInfo.packageName) != null
}

View file

@ -53,7 +53,7 @@ internal class DocumentsProviderRestorePlugin(
}
@WorkerThread
fun getBackups(context: Context, rootDir: DocumentFile): List<BackupSet> {
private fun getBackups(context: Context, rootDir: DocumentFile): List<BackupSet> {
val backupSets = ArrayList<BackupSet>()
val files = try {
// block until the DocumentsProvider has results

View file

@ -204,6 +204,7 @@ fun DocumentFile.listFilesBlocking(context: Context): ArrayList<DocumentFile> {
})
val timeout = MINUTES.toMillis(2)
var time = 0
// TODO replace loop with callback flow or something similar
while (!loaded && time < timeout) {
Thread.sleep(50)
time += 50

View file

@ -49,6 +49,7 @@ class SettingsManager(context: Context) {
return Storage(uri, name, isUsb)
}
// TODO find a better solution for this hack abusing the settings manager
fun getAndResetIsStorageChanging(): Boolean {
return isStorageChanging.getAndSet(false)
}

View file

@ -15,6 +15,8 @@ interface KVRestorePlugin {
/**
* Return all record keys for the given token and package.
*
* Note: Implementations might expect that you call [hasDataForPackage] before.
*
* For file-based plugins, this is usually a list of file names in the package directory.
*/
@Throws(IOException::class)
@ -23,6 +25,8 @@ interface KVRestorePlugin {
/**
* Return an [InputStream] for the given token, package and key
* which will provide the record's encrypted value.
*
* Note: Implementations might expect that you call [hasDataForPackage] before.
*/
@Throws(IOException::class)
fun getInputStreamForRecord(token: Long, packageInfo: PackageInfo, key: String): InputStream

View file

@ -19,6 +19,16 @@ fun getRandomString(size: Int = Random.nextInt(1, 255)): String {
.joinToString("")
}
// URL-save version (RFC 4648)
private val base64CharPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9') + '+' + '_' + '='
fun getRandomBase64(size: Int = Random.nextInt(1, 255)): String {
return (1..size)
.map { Random.nextInt(0, base64CharPool.size) }
.map(base64CharPool::get)
.joinToString("")
}
fun ByteArray.toHexString(): String {
var str = ""
for (b in this) {