Read RestoreSets from encrypted backup metadata file
This commit is contained in:
parent
f9c8b657a0
commit
8b6656a350
10 changed files with 126 additions and 30 deletions
|
@ -4,6 +4,7 @@ import android.os.Build
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import com.stevesoltys.backup.header.VERSION
|
import com.stevesoltys.backup.header.VERSION
|
||||||
import com.stevesoltys.backup.transport.DEFAULT_RESTORE_SET_TOKEN
|
import com.stevesoltys.backup.transport.DEFAULT_RESTORE_SET_TOKEN
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
data class BackupMetadata(
|
data class BackupMetadata(
|
||||||
internal val version: Byte = VERSION,
|
internal val version: Byte = VERSION,
|
||||||
|
@ -18,3 +19,8 @@ internal const val JSON_ANDROID_VERSION = "androidVersion"
|
||||||
internal const val JSON_DEVICE_NAME = "deviceName"
|
internal const val JSON_DEVICE_NAME = "deviceName"
|
||||||
|
|
||||||
class FormatException(cause: Throwable) : Exception(cause)
|
class FormatException(cause: Throwable) : Exception(cause)
|
||||||
|
|
||||||
|
class EncryptedBackupMetadata private constructor(val token: Long, val inputStream: InputStream?, val error: Boolean) {
|
||||||
|
constructor(token: Long, inputStream: InputStream) : this(token, inputStream, false)
|
||||||
|
constructor(token: Long, error: Boolean) : this(token, null, false)
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,36 @@
|
||||||
package com.stevesoltys.backup.metadata
|
package com.stevesoltys.backup.metadata
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.stevesoltys.backup.Utf8
|
import com.stevesoltys.backup.Utf8
|
||||||
|
import com.stevesoltys.backup.crypto.Crypto
|
||||||
|
import com.stevesoltys.backup.header.UnsupportedVersionException
|
||||||
|
import com.stevesoltys.backup.header.VERSION
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
interface MetadataDecoder {
|
interface MetadataReader {
|
||||||
|
|
||||||
@Throws(FormatException::class, SecurityException::class)
|
@Throws(FormatException::class, SecurityException::class, UnsupportedVersionException::class, IOException::class)
|
||||||
fun decode(bytes: ByteArray, expectedVersion: Byte, expectedToken: Long): BackupMetadata
|
fun readMetadata(inputStream: InputStream, expectedToken: Long): BackupMetadata
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MetadataDecoderImpl : MetadataDecoder {
|
class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
||||||
|
|
||||||
|
@Throws(FormatException::class, SecurityException::class, UnsupportedVersionException::class, IOException::class)
|
||||||
|
override fun readMetadata(inputStream: InputStream, expectedToken: Long): BackupMetadata {
|
||||||
|
val version = inputStream.read().toByte()
|
||||||
|
if (version < 0) throw IOException()
|
||||||
|
if (version > VERSION) throw UnsupportedVersionException(version)
|
||||||
|
val metadataBytes = crypto.decryptSegment(inputStream)
|
||||||
|
return decode(metadataBytes, version, expectedToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
@Throws(FormatException::class, SecurityException::class)
|
@Throws(FormatException::class, SecurityException::class)
|
||||||
override fun decode(bytes: ByteArray, expectedVersion: Byte, expectedToken: Long): BackupMetadata {
|
internal fun decode(bytes: ByteArray, expectedVersion: Byte, expectedToken: Long): BackupMetadata {
|
||||||
// NOTE: We don't do extensive validation of the parsed input here,
|
// NOTE: We don't do extensive validation of the parsed input here,
|
||||||
// because it was encrypted with authentication, so we should be able to trust it.
|
// because it was encrypted with authentication, so we should be able to trust it.
|
||||||
//
|
//
|
||||||
|
@ -28,7 +44,7 @@ class MetadataDecoderImpl : MetadataDecoder {
|
||||||
}
|
}
|
||||||
val token = json.getLong(JSON_TOKEN)
|
val token = json.getLong(JSON_TOKEN)
|
||||||
if (token != expectedToken) {
|
if (token != expectedToken) {
|
||||||
throw SecurityException("Invalid token '$expectedVersion' in metadata, expected '$expectedToken'.")
|
throw SecurityException("Invalid token '$token' in metadata, expected '$expectedToken'.")
|
||||||
}
|
}
|
||||||
return BackupMetadata(
|
return BackupMetadata(
|
||||||
version = version,
|
version = version,
|
|
@ -6,6 +6,7 @@ import com.stevesoltys.backup.crypto.CipherFactoryImpl
|
||||||
import com.stevesoltys.backup.crypto.CryptoImpl
|
import com.stevesoltys.backup.crypto.CryptoImpl
|
||||||
import com.stevesoltys.backup.header.HeaderReaderImpl
|
import com.stevesoltys.backup.header.HeaderReaderImpl
|
||||||
import com.stevesoltys.backup.header.HeaderWriterImpl
|
import com.stevesoltys.backup.header.HeaderWriterImpl
|
||||||
|
import com.stevesoltys.backup.metadata.MetadataReaderImpl
|
||||||
import com.stevesoltys.backup.metadata.MetadataWriterImpl
|
import com.stevesoltys.backup.metadata.MetadataWriterImpl
|
||||||
import com.stevesoltys.backup.settings.getBackupFolderUri
|
import com.stevesoltys.backup.settings.getBackupFolderUri
|
||||||
import com.stevesoltys.backup.settings.getDeviceName
|
import com.stevesoltys.backup.settings.getDeviceName
|
||||||
|
@ -32,6 +33,7 @@ class PluginManager(context: Context) {
|
||||||
private val cipherFactory = CipherFactoryImpl(Backup.keyManager)
|
private val cipherFactory = CipherFactoryImpl(Backup.keyManager)
|
||||||
private val crypto = CryptoImpl(cipherFactory, headerWriter, headerReader)
|
private val crypto = CryptoImpl(cipherFactory, headerWriter, headerReader)
|
||||||
private val metadataWriter = MetadataWriterImpl(crypto)
|
private val metadataWriter = MetadataWriterImpl(crypto)
|
||||||
|
private val metadataReader = MetadataReaderImpl(crypto)
|
||||||
|
|
||||||
|
|
||||||
private val backupPlugin = DocumentsProviderBackupPlugin(storage, context.packageManager)
|
private val backupPlugin = DocumentsProviderBackupPlugin(storage, context.packageManager)
|
||||||
|
@ -48,6 +50,6 @@ class PluginManager(context: Context) {
|
||||||
private val kvRestore = KVRestore(restorePlugin.kvRestorePlugin, outputFactory, headerReader, crypto)
|
private val kvRestore = KVRestore(restorePlugin.kvRestorePlugin, outputFactory, headerReader, crypto)
|
||||||
private val fullRestore = FullRestore(restorePlugin.fullRestorePlugin, outputFactory, headerReader, crypto)
|
private val fullRestore = FullRestore(restorePlugin.fullRestorePlugin, outputFactory, headerReader, crypto)
|
||||||
|
|
||||||
internal val restoreCoordinator = RestoreCoordinator(restorePlugin, kvRestore, fullRestore)
|
internal val restoreCoordinator = RestoreCoordinator(restorePlugin, kvRestore, fullRestore, metadataReader)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,10 @@ import android.app.backup.RestoreSet
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.stevesoltys.backup.header.UnsupportedVersionException
|
||||||
|
import com.stevesoltys.backup.metadata.FormatException
|
||||||
|
import com.stevesoltys.backup.metadata.MetadataReader
|
||||||
|
import libcore.io.IoUtils.closeQuietly
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
private class RestoreCoordinatorState(
|
private class RestoreCoordinatorState(
|
||||||
|
@ -19,13 +23,45 @@ private val TAG = RestoreCoordinator::class.java.simpleName
|
||||||
internal class RestoreCoordinator(
|
internal class RestoreCoordinator(
|
||||||
private val plugin: RestorePlugin,
|
private val plugin: RestorePlugin,
|
||||||
private val kv: KVRestore,
|
private val kv: KVRestore,
|
||||||
private val full: FullRestore) {
|
private val full: FullRestore,
|
||||||
|
private val metadataReader: MetadataReader) {
|
||||||
|
|
||||||
private var state: RestoreCoordinatorState? = null
|
private var state: RestoreCoordinatorState? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the set of all backups currently available over this transport.
|
||||||
|
*
|
||||||
|
* @return Descriptions of the set of restore images available for this device,
|
||||||
|
* or null if an error occurred (the attempt should be rescheduled).
|
||||||
|
**/
|
||||||
fun getAvailableRestoreSets(): Array<RestoreSet>? {
|
fun getAvailableRestoreSets(): Array<RestoreSet>? {
|
||||||
return plugin.getAvailableRestoreSets()
|
val availableBackups = plugin.getAvailableBackups() ?: return null
|
||||||
.apply { Log.i(TAG, "Got available restore sets: $this") }
|
val restoreSets = ArrayList<RestoreSet>()
|
||||||
|
for (encryptedMetadata in availableBackups) {
|
||||||
|
if (encryptedMetadata.error) continue
|
||||||
|
check(encryptedMetadata.inputStream != null) // if there's no error, there must be a stream
|
||||||
|
try {
|
||||||
|
val metadata = metadataReader.readMetadata(encryptedMetadata.inputStream, encryptedMetadata.token)
|
||||||
|
val set = RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token)
|
||||||
|
restoreSets.add(set)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Error while getting restore sets", e)
|
||||||
|
return null
|
||||||
|
} catch (e: FormatException) {
|
||||||
|
Log.e(TAG, "Error while getting restore sets", e)
|
||||||
|
return null
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.e(TAG, "Error while getting restore sets", e)
|
||||||
|
return null
|
||||||
|
} catch (e: UnsupportedVersionException) {
|
||||||
|
Log.w(TAG, "Backup with unsupported version read", e)
|
||||||
|
continue
|
||||||
|
} finally {
|
||||||
|
closeQuietly(encryptedMetadata.inputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Got available restore sets: $restoreSets")
|
||||||
|
return restoreSets.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentRestoreSet(): Long {
|
fun getCurrentRestoreSet(): Long {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package com.stevesoltys.backup.transport.restore
|
package com.stevesoltys.backup.transport.restore
|
||||||
|
|
||||||
import android.app.backup.RestoreSet
|
import com.stevesoltys.backup.metadata.EncryptedBackupMetadata
|
||||||
|
|
||||||
interface RestorePlugin {
|
interface RestorePlugin {
|
||||||
|
|
||||||
|
@ -11,10 +11,10 @@ interface RestorePlugin {
|
||||||
/**
|
/**
|
||||||
* Get the set of all backups currently available for restore.
|
* Get the set of all backups currently available for restore.
|
||||||
*
|
*
|
||||||
* @return Descriptions of the set of restore images available for this device,
|
* @return metadata for the set of restore images available,
|
||||||
* or null if an error occurred (the attempt should be rescheduled).
|
* or null if an error occurred (the attempt should be rescheduled).
|
||||||
**/
|
**/
|
||||||
fun getAvailableRestoreSets(): Array<RestoreSet>?
|
fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the identifying token of the backup set currently being stored from this device.
|
* Get the identifying token of the backup set currently being stored from this device.
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
package com.stevesoltys.backup.transport.restore.plugins
|
package com.stevesoltys.backup.transport.restore.plugins
|
||||||
|
|
||||||
import android.app.backup.RestoreSet
|
import android.util.Log
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.stevesoltys.backup.metadata.EncryptedBackupMetadata
|
||||||
import com.stevesoltys.backup.transport.DEFAULT_RESTORE_SET_TOKEN
|
import com.stevesoltys.backup.transport.DEFAULT_RESTORE_SET_TOKEN
|
||||||
import com.stevesoltys.backup.transport.backup.plugins.DocumentsStorage
|
import com.stevesoltys.backup.transport.backup.plugins.DocumentsStorage
|
||||||
|
import com.stevesoltys.backup.transport.backup.plugins.FILE_BACKUP_METADATA
|
||||||
import com.stevesoltys.backup.transport.restore.FullRestorePlugin
|
import com.stevesoltys.backup.transport.restore.FullRestorePlugin
|
||||||
import com.stevesoltys.backup.transport.restore.KVRestorePlugin
|
import com.stevesoltys.backup.transport.restore.KVRestorePlugin
|
||||||
import com.stevesoltys.backup.transport.restore.RestorePlugin
|
import com.stevesoltys.backup.transport.restore.RestorePlugin
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
private val TAG = DocumentsProviderRestorePlugin::class.java.simpleName
|
||||||
|
|
||||||
class DocumentsProviderRestorePlugin(private val storage: DocumentsStorage) : RestorePlugin {
|
class DocumentsProviderRestorePlugin(private val storage: DocumentsStorage) : RestorePlugin {
|
||||||
|
|
||||||
|
@ -17,16 +23,28 @@ class DocumentsProviderRestorePlugin(private val storage: DocumentsStorage) : Re
|
||||||
DocumentsProviderFullRestorePlugin(storage)
|
DocumentsProviderFullRestorePlugin(storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAvailableRestoreSets(): Array<RestoreSet>? {
|
override fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? {
|
||||||
val rootDir = storage.rootBackupDir ?: return null
|
val rootDir = storage.rootBackupDir ?: return null
|
||||||
val restoreSets = ArrayList<RestoreSet>()
|
val files = ArrayList<DocumentFile>()
|
||||||
for (file in rootDir.listFiles()) {
|
for (file in rootDir.listFiles()) {
|
||||||
if (file.isDirectory && file.findFile(DEFAULT_RESTORE_SET_TOKEN.toString()) != null) {
|
file.isDirectory || continue
|
||||||
// TODO include time of last backup
|
val set = file.findFile(DEFAULT_RESTORE_SET_TOKEN.toString()) ?: continue
|
||||||
file.name?.let { restoreSets.add(RestoreSet(it, it, DEFAULT_RESTORE_SET_TOKEN)) }
|
val metadata = set.findFile(FILE_BACKUP_METADATA) ?: continue
|
||||||
|
files.add(metadata)
|
||||||
|
}
|
||||||
|
val iterator = files.iterator()
|
||||||
|
return generateSequence {
|
||||||
|
if (!iterator.hasNext()) return@generateSequence null // end sequence
|
||||||
|
val metadata = iterator.next()
|
||||||
|
val token = metadata.parentFile!!.name!!.toLong()
|
||||||
|
try {
|
||||||
|
val stream = storage.getInputStream(metadata)
|
||||||
|
EncryptedBackupMetadata(token, stream)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Error getting InputStream for backup metadata.", e)
|
||||||
|
EncryptedBackupMetadata(token, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return restoreSets.toTypedArray()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentRestoreSet(): Long {
|
override fun getCurrentRestoreSet(): Long {
|
||||||
|
|
|
@ -12,12 +12,12 @@ import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@TestInstance(PER_CLASS)
|
@TestInstance(PER_CLASS)
|
||||||
class MetadataDecoderTest {
|
class MetadataReaderTest {
|
||||||
|
|
||||||
private val crypto = mockk<Crypto>()
|
private val crypto = mockk<Crypto>()
|
||||||
|
|
||||||
private val encoder = MetadataWriterImpl(crypto)
|
private val encoder = MetadataWriterImpl(crypto)
|
||||||
private val decoder = MetadataDecoderImpl()
|
private val decoder = MetadataReaderImpl(crypto)
|
||||||
|
|
||||||
private val metadata = BackupMetadata(
|
private val metadata = BackupMetadata(
|
||||||
version = 1.toByte(),
|
version = 1.toByte(),
|
|
@ -15,7 +15,7 @@ internal class MetadataWriterDecoderTest {
|
||||||
private val crypto = mockk<Crypto>()
|
private val crypto = mockk<Crypto>()
|
||||||
|
|
||||||
private val encoder = MetadataWriterImpl(crypto)
|
private val encoder = MetadataWriterImpl(crypto)
|
||||||
private val decoder = MetadataDecoderImpl()
|
private val decoder = MetadataReaderImpl(crypto)
|
||||||
|
|
||||||
private val metadata = BackupMetadata(
|
private val metadata = BackupMetadata(
|
||||||
version = Random.nextBytes(1)[0],
|
version = Random.nextBytes(1)[0],
|
||||||
|
|
|
@ -14,6 +14,7 @@ import com.stevesoltys.backup.crypto.KeyManagerTestImpl
|
||||||
import com.stevesoltys.backup.encodeBase64
|
import com.stevesoltys.backup.encodeBase64
|
||||||
import com.stevesoltys.backup.header.HeaderReaderImpl
|
import com.stevesoltys.backup.header.HeaderReaderImpl
|
||||||
import com.stevesoltys.backup.header.HeaderWriterImpl
|
import com.stevesoltys.backup.header.HeaderWriterImpl
|
||||||
|
import com.stevesoltys.backup.metadata.MetadataReaderImpl
|
||||||
import com.stevesoltys.backup.metadata.MetadataWriterImpl
|
import com.stevesoltys.backup.metadata.MetadataWriterImpl
|
||||||
import com.stevesoltys.backup.transport.backup.*
|
import com.stevesoltys.backup.transport.backup.*
|
||||||
import com.stevesoltys.backup.transport.restore.*
|
import com.stevesoltys.backup.transport.restore.*
|
||||||
|
@ -34,6 +35,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
private val headerReader = HeaderReaderImpl()
|
private val headerReader = HeaderReaderImpl()
|
||||||
private val cryptoImpl = CryptoImpl(cipherFactory, headerWriter, headerReader)
|
private val cryptoImpl = CryptoImpl(cipherFactory, headerWriter, headerReader)
|
||||||
private val metadataWriter = MetadataWriterImpl(cryptoImpl)
|
private val metadataWriter = MetadataWriterImpl(cryptoImpl)
|
||||||
|
private val metadataReader = MetadataReaderImpl(cryptoImpl)
|
||||||
|
|
||||||
private val backupPlugin = mockk<BackupPlugin>()
|
private val backupPlugin = mockk<BackupPlugin>()
|
||||||
private val kvBackupPlugin = mockk<KVBackupPlugin>()
|
private val kvBackupPlugin = mockk<KVBackupPlugin>()
|
||||||
|
@ -48,7 +50,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
||||||
private val fullRestorePlugin = mockk<FullRestorePlugin>()
|
private val fullRestorePlugin = mockk<FullRestorePlugin>()
|
||||||
private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
||||||
private val restore = RestoreCoordinator(restorePlugin, kvRestore, fullRestore)
|
private val restore = RestoreCoordinator(restorePlugin, kvRestore, fullRestore, metadataReader)
|
||||||
|
|
||||||
private val backupDataInput = mockk<BackupDataInput>()
|
private val backupDataInput = mockk<BackupDataInput>()
|
||||||
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
|
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||||
|
|
|
@ -3,9 +3,12 @@ package com.stevesoltys.backup.transport.restore
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||||
import android.app.backup.RestoreDescription
|
import android.app.backup.RestoreDescription
|
||||||
import android.app.backup.RestoreDescription.*
|
import android.app.backup.RestoreDescription.*
|
||||||
import android.app.backup.RestoreSet
|
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
|
import com.stevesoltys.backup.getRandomString
|
||||||
|
import com.stevesoltys.backup.metadata.BackupMetadata
|
||||||
|
import com.stevesoltys.backup.metadata.EncryptedBackupMetadata
|
||||||
|
import com.stevesoltys.backup.metadata.MetadataReader
|
||||||
import com.stevesoltys.backup.transport.TransportTest
|
import com.stevesoltys.backup.transport.TransportTest
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
@ -14,6 +17,7 @@ import io.mockk.mockk
|
||||||
import org.junit.jupiter.api.Assertions.*
|
import org.junit.jupiter.api.Assertions.*
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
internal class RestoreCoordinatorTest : TransportTest() {
|
internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
|
@ -21,21 +25,33 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
private val plugin = mockk<RestorePlugin>()
|
private val plugin = mockk<RestorePlugin>()
|
||||||
private val kv = mockk<KVRestore>()
|
private val kv = mockk<KVRestore>()
|
||||||
private val full = mockk<FullRestore>()
|
private val full = mockk<FullRestore>()
|
||||||
|
private val metadataReader = mockk<MetadataReader>()
|
||||||
|
|
||||||
private val restore = RestoreCoordinator(plugin, kv, full)
|
private val restore = RestoreCoordinator(plugin, kv, full, metadataReader)
|
||||||
|
|
||||||
private val token = Random.nextLong()
|
private val token = Random.nextLong()
|
||||||
|
private val inputStream = mockk<InputStream>()
|
||||||
private val packageInfo2 = PackageInfo().apply { packageName = "org.example2" }
|
private val packageInfo2 = PackageInfo().apply { packageName = "org.example2" }
|
||||||
private val packageInfoArray = arrayOf(packageInfo)
|
private val packageInfoArray = arrayOf(packageInfo)
|
||||||
private val packageInfoArray2 = arrayOf(packageInfo, packageInfo2)
|
private val packageInfoArray2 = arrayOf(packageInfo, packageInfo2)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getAvailableRestoreSets() delegates to plugin`() {
|
fun `getAvailableRestoreSets() builds set from plugin response`() {
|
||||||
val restoreSets = Array(1) { RestoreSet() }
|
val encryptedMetadata = EncryptedBackupMetadata(token, inputStream)
|
||||||
|
val metadata = BackupMetadata(
|
||||||
|
token = token,
|
||||||
|
androidVersion = Random.nextInt(),
|
||||||
|
deviceName = getRandomString())
|
||||||
|
|
||||||
every { plugin.getAvailableRestoreSets() } returns restoreSets
|
every { plugin.getAvailableBackups() } returns sequenceOf(encryptedMetadata, encryptedMetadata)
|
||||||
|
every { metadataReader.readMetadata(inputStream, token) } returns metadata
|
||||||
|
every { inputStream.close() } just Runs
|
||||||
|
|
||||||
assertEquals(restoreSets, restore.getAvailableRestoreSets())
|
val sets = restore.getAvailableRestoreSets() ?: fail()
|
||||||
|
assertEquals(2, sets.size)
|
||||||
|
assertEquals(metadata.deviceName, sets[0].device)
|
||||||
|
assertEquals(metadata.deviceName, sets[0].name)
|
||||||
|
assertEquals(metadata.token, sets[0].token)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in a new issue