Write an encrypted metadata file for each restore set
This commit is contained in:
parent
044ef01ba1
commit
f9c8b657a0
12 changed files with 250 additions and 6 deletions
|
@ -0,0 +1,20 @@
|
||||||
|
package com.stevesoltys.backup.metadata
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Build.VERSION.SDK_INT
|
||||||
|
import com.stevesoltys.backup.header.VERSION
|
||||||
|
import com.stevesoltys.backup.transport.DEFAULT_RESTORE_SET_TOKEN
|
||||||
|
|
||||||
|
data class BackupMetadata(
|
||||||
|
internal val version: Byte = VERSION,
|
||||||
|
internal val token: Long = DEFAULT_RESTORE_SET_TOKEN,
|
||||||
|
internal val androidVersion: Int = SDK_INT,
|
||||||
|
internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}"
|
||||||
|
)
|
||||||
|
|
||||||
|
internal const val JSON_VERSION = "version"
|
||||||
|
internal const val JSON_TOKEN = "token"
|
||||||
|
internal const val JSON_ANDROID_VERSION = "androidVersion"
|
||||||
|
internal const val JSON_DEVICE_NAME = "deviceName"
|
||||||
|
|
||||||
|
class FormatException(cause: Throwable) : Exception(cause)
|
|
@ -0,0 +1,44 @@
|
||||||
|
package com.stevesoltys.backup.metadata
|
||||||
|
|
||||||
|
import com.stevesoltys.backup.Utf8
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
interface MetadataDecoder {
|
||||||
|
|
||||||
|
@Throws(FormatException::class, SecurityException::class)
|
||||||
|
fun decode(bytes: ByteArray, expectedVersion: Byte, expectedToken: Long): BackupMetadata
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class MetadataDecoderImpl : MetadataDecoder {
|
||||||
|
|
||||||
|
@Throws(FormatException::class, SecurityException::class)
|
||||||
|
override fun decode(bytes: ByteArray, expectedVersion: Byte, expectedToken: Long): BackupMetadata {
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// However, it is important to ensure that the expected unauthenticated version and token
|
||||||
|
// matches the authenticated version and token in the JSON.
|
||||||
|
try {
|
||||||
|
val json = JSONObject(bytes.toString(Utf8))
|
||||||
|
val version = json.getInt(JSON_VERSION).toByte()
|
||||||
|
if (version != expectedVersion) {
|
||||||
|
throw SecurityException("Invalid version '${version.toInt()}' in metadata, expected '${expectedVersion.toInt()}'.")
|
||||||
|
}
|
||||||
|
val token = json.getLong(JSON_TOKEN)
|
||||||
|
if (token != expectedToken) {
|
||||||
|
throw SecurityException("Invalid token '$expectedVersion' in metadata, expected '$expectedToken'.")
|
||||||
|
}
|
||||||
|
return BackupMetadata(
|
||||||
|
version = version,
|
||||||
|
token = token,
|
||||||
|
androidVersion = json.getInt(JSON_ANDROID_VERSION),
|
||||||
|
deviceName = json.getString(JSON_DEVICE_NAME)
|
||||||
|
)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
throw FormatException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.stevesoltys.backup.metadata
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import com.stevesoltys.backup.Utf8
|
||||||
|
import com.stevesoltys.backup.crypto.Crypto
|
||||||
|
import com.stevesoltys.backup.transport.DEFAULT_RESTORE_SET_TOKEN
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
interface MetadataWriter {
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun write(outputStream: OutputStream, token: Long = DEFAULT_RESTORE_SET_TOKEN)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class MetadataWriterImpl(private val crypto: Crypto): MetadataWriter {
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun write(outputStream: OutputStream, token: Long) {
|
||||||
|
val metadata = BackupMetadata(token = token)
|
||||||
|
outputStream.write(ByteArray(1).apply { this[0] = metadata.version })
|
||||||
|
crypto.encryptSegment(outputStream, encode(metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun encode(metadata: BackupMetadata): ByteArray {
|
||||||
|
val json = JSONObject()
|
||||||
|
json.put(JSON_VERSION, metadata.version.toInt())
|
||||||
|
json.put(JSON_TOKEN, metadata.token)
|
||||||
|
json.put(JSON_ANDROID_VERSION, metadata.androidVersion)
|
||||||
|
json.put(JSON_DEVICE_NAME, metadata.deviceName)
|
||||||
|
return json.toString().toByteArray(Utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.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
|
||||||
import com.stevesoltys.backup.transport.backup.BackupCoordinator
|
import com.stevesoltys.backup.transport.backup.BackupCoordinator
|
||||||
|
@ -30,6 +31,7 @@ class PluginManager(context: Context) {
|
||||||
private val headerReader = HeaderReaderImpl()
|
private val headerReader = HeaderReaderImpl()
|
||||||
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 backupPlugin = DocumentsProviderBackupPlugin(storage, context.packageManager)
|
private val backupPlugin = DocumentsProviderBackupPlugin(storage, context.packageManager)
|
||||||
|
@ -38,7 +40,7 @@ class PluginManager(context: Context) {
|
||||||
private val fullBackup = FullBackup(backupPlugin.fullBackupPlugin, inputFactory, headerWriter, crypto)
|
private val fullBackup = FullBackup(backupPlugin.fullBackupPlugin, inputFactory, headerWriter, crypto)
|
||||||
private val notificationManager = (context.applicationContext as Backup).notificationManager
|
private val notificationManager = (context.applicationContext as Backup).notificationManager
|
||||||
|
|
||||||
internal val backupCoordinator = BackupCoordinator(backupPlugin, kvBackup, fullBackup, notificationManager)
|
internal val backupCoordinator = BackupCoordinator(backupPlugin, kvBackup, fullBackup, metadataWriter, notificationManager)
|
||||||
|
|
||||||
|
|
||||||
private val restorePlugin = DocumentsProviderRestorePlugin(storage)
|
private val restorePlugin = DocumentsProviderRestorePlugin(storage)
|
||||||
|
|
|
@ -6,6 +6,7 @@ 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.BackupNotificationManager
|
import com.stevesoltys.backup.BackupNotificationManager
|
||||||
|
import com.stevesoltys.backup.metadata.MetadataWriter
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
private val TAG = BackupCoordinator::class.java.simpleName
|
private val TAG = BackupCoordinator::class.java.simpleName
|
||||||
|
@ -18,6 +19,7 @@ class BackupCoordinator(
|
||||||
private val plugin: BackupPlugin,
|
private val plugin: BackupPlugin,
|
||||||
private val kv: KVBackup,
|
private val kv: KVBackup,
|
||||||
private val full: FullBackup,
|
private val full: FullBackup,
|
||||||
|
private val metadataWriter: MetadataWriter,
|
||||||
private val nm: BackupNotificationManager) {
|
private val nm: BackupNotificationManager) {
|
||||||
|
|
||||||
private var calledInitialize = false
|
private var calledInitialize = false
|
||||||
|
@ -49,6 +51,7 @@ class BackupCoordinator(
|
||||||
Log.i(TAG, "Initialize Device!")
|
Log.i(TAG, "Initialize Device!")
|
||||||
return try {
|
return try {
|
||||||
plugin.initializeDevice()
|
plugin.initializeDevice()
|
||||||
|
writeBackupMetadata()
|
||||||
// [finishBackup] will only be called when we return [TRANSPORT_OK] here
|
// [finishBackup] will only be called when we return [TRANSPORT_OK] here
|
||||||
// so we remember that we initialized successfully
|
// so we remember that we initialized successfully
|
||||||
calledInitialize = true
|
calledInitialize = true
|
||||||
|
@ -129,11 +132,11 @@ class BackupCoordinator(
|
||||||
|
|
||||||
fun finishBackup(): Int = when {
|
fun finishBackup(): Int = when {
|
||||||
kv.hasState() -> {
|
kv.hasState() -> {
|
||||||
if (full.hasState()) throw IllegalStateException()
|
check(!full.hasState())
|
||||||
kv.finishBackup()
|
kv.finishBackup()
|
||||||
}
|
}
|
||||||
full.hasState() -> {
|
full.hasState() -> {
|
||||||
if (kv.hasState()) throw IllegalStateException()
|
check(!kv.hasState())
|
||||||
full.finishBackup()
|
full.finishBackup()
|
||||||
}
|
}
|
||||||
calledInitialize || calledClearBackupData -> {
|
calledInitialize || calledClearBackupData -> {
|
||||||
|
@ -144,4 +147,10 @@ class BackupCoordinator(
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun writeBackupMetadata() {
|
||||||
|
val outputStream = plugin.getMetadataOutputStream()
|
||||||
|
metadataWriter.write(outputStream)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.stevesoltys.backup.transport.backup
|
package com.stevesoltys.backup.transport.backup
|
||||||
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
interface BackupPlugin {
|
interface BackupPlugin {
|
||||||
|
|
||||||
|
@ -14,6 +15,12 @@ interface BackupPlugin {
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun initializeDevice()
|
fun initializeDevice()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an [OutputStream] for writing backup metadata.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getMetadataOutputStream(): OutputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the package name of the app that provides the backend storage
|
* Returns the package name of the app that provides the backend storage
|
||||||
* which is used for the current backup location.
|
* which is used for the current backup location.
|
||||||
|
|
|
@ -5,6 +5,7 @@ import com.stevesoltys.backup.transport.backup.BackupPlugin
|
||||||
import com.stevesoltys.backup.transport.backup.FullBackupPlugin
|
import com.stevesoltys.backup.transport.backup.FullBackupPlugin
|
||||||
import com.stevesoltys.backup.transport.backup.KVBackupPlugin
|
import com.stevesoltys.backup.transport.backup.KVBackupPlugin
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
class DocumentsProviderBackupPlugin(
|
class DocumentsProviderBackupPlugin(
|
||||||
private val storage: DocumentsStorage,
|
private val storage: DocumentsStorage,
|
||||||
|
@ -28,10 +29,18 @@ class DocumentsProviderBackupPlugin(
|
||||||
val fullDir = storage.defaultFullBackupDir
|
val fullDir = storage.defaultFullBackupDir
|
||||||
|
|
||||||
// wipe existing data
|
// wipe existing data
|
||||||
|
storage.getSetDir()?.findFile(FILE_BACKUP_METADATA)?.delete()
|
||||||
kvDir?.deleteContents()
|
kvDir?.deleteContents()
|
||||||
fullDir?.deleteContents()
|
fullDir?.deleteContents()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun getMetadataOutputStream(): OutputStream {
|
||||||
|
val setDir = storage.getSetDir() ?: throw IOException()
|
||||||
|
val metadataFile = setDir.createOrGetFile(FILE_BACKUP_METADATA)
|
||||||
|
return storage.getOutputStream(metadataFile)
|
||||||
|
}
|
||||||
|
|
||||||
override val providerPackageName: String? by lazy {
|
override val providerPackageName: String? by lazy {
|
||||||
val authority = storage.rootBackupDir?.uri?.authority ?: return@lazy null
|
val authority = storage.rootBackupDir?.uri?.authority ?: return@lazy null
|
||||||
val providerInfo = packageManager.resolveContentProvider(authority, 0) ?: return@lazy null
|
val providerInfo = packageManager.resolveContentProvider(authority, 0) ?: return@lazy null
|
||||||
|
|
|
@ -12,6 +12,7 @@ import java.io.OutputStream
|
||||||
|
|
||||||
const val DIRECTORY_FULL_BACKUP = "full"
|
const val DIRECTORY_FULL_BACKUP = "full"
|
||||||
const val DIRECTORY_KEY_VALUE_BACKUP = "kv"
|
const val DIRECTORY_KEY_VALUE_BACKUP = "kv"
|
||||||
|
const val FILE_BACKUP_METADATA = ".backup.metadata"
|
||||||
private const val ROOT_DIR_NAME = ".AndroidBackup"
|
private const val ROOT_DIR_NAME = ".AndroidBackup"
|
||||||
private const val NO_MEDIA = ".nomedia"
|
private const val NO_MEDIA = ".nomedia"
|
||||||
private const val MIME_TYPE = "application/octet-stream"
|
private const val MIME_TYPE = "application/octet-stream"
|
||||||
|
@ -24,6 +25,7 @@ class DocumentsStorage(context: Context, parentFolder: Uri?, deviceName: String)
|
||||||
|
|
||||||
internal val rootBackupDir: DocumentFile? by lazy {
|
internal val rootBackupDir: DocumentFile? by lazy {
|
||||||
val folderUri = parentFolder ?: return@lazy null
|
val folderUri = parentFolder ?: return@lazy null
|
||||||
|
// [fromTreeUri] should only return null when SDK_INT < 21
|
||||||
val parent = DocumentFile.fromTreeUri(context, folderUri) ?: throw AssertionError()
|
val parent = DocumentFile.fromTreeUri(context, folderUri) ?: throw AssertionError()
|
||||||
try {
|
try {
|
||||||
val rootDir = parent.createOrGetDirectory(ROOT_DIR_NAME)
|
val rootDir = parent.createOrGetDirectory(ROOT_DIR_NAME)
|
||||||
|
@ -73,7 +75,7 @@ class DocumentsStorage(context: Context, parentFolder: Uri?, deviceName: String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSetDir(token: Long = DEFAULT_RESTORE_SET_TOKEN): DocumentFile? {
|
fun getSetDir(token: Long = DEFAULT_RESTORE_SET_TOKEN): DocumentFile? {
|
||||||
if (token == DEFAULT_RESTORE_SET_TOKEN) return defaultSetDir
|
if (token == DEFAULT_RESTORE_SET_TOKEN) return defaultSetDir
|
||||||
return deviceDir?.findFile(token.toString())
|
return deviceDir?.findFile(token.toString())
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
package com.stevesoltys.backup.metadata
|
||||||
|
|
||||||
|
import com.stevesoltys.backup.Utf8
|
||||||
|
import com.stevesoltys.backup.crypto.Crypto
|
||||||
|
import com.stevesoltys.backup.getRandomString
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@TestInstance(PER_CLASS)
|
||||||
|
class MetadataDecoderTest {
|
||||||
|
|
||||||
|
private val crypto = mockk<Crypto>()
|
||||||
|
|
||||||
|
private val encoder = MetadataWriterImpl(crypto)
|
||||||
|
private val decoder = MetadataDecoderImpl()
|
||||||
|
|
||||||
|
private val metadata = BackupMetadata(
|
||||||
|
version = 1.toByte(),
|
||||||
|
token = Random.nextLong(),
|
||||||
|
androidVersion = Random.nextInt(),
|
||||||
|
deviceName = getRandomString()
|
||||||
|
)
|
||||||
|
private val metadataByteArray = encoder.encode(metadata)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unexpected version should throw SecurityException`() {
|
||||||
|
assertThrows(SecurityException::class.java) {
|
||||||
|
decoder.decode(metadataByteArray, 2.toByte(), metadata.token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unexpected token should throw SecurityException`() {
|
||||||
|
assertThrows(SecurityException::class.java) {
|
||||||
|
decoder.decode(metadataByteArray, metadata.version, metadata.token - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `expected version and token do not throw SecurityException`() {
|
||||||
|
decoder.decode(metadataByteArray, metadata.version, metadata.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `malformed JSON throws FormatException`() {
|
||||||
|
assertThrows(FormatException::class.java) {
|
||||||
|
decoder.decode("{".toByteArray(Utf8), metadata.version, metadata.token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `missing fields throws FormatException`() {
|
||||||
|
val json = JSONObject()
|
||||||
|
json.put(JSON_VERSION, metadata.version.toInt())
|
||||||
|
json.put(JSON_TOKEN, metadata.token)
|
||||||
|
json.put(JSON_ANDROID_VERSION, metadata.androidVersion)
|
||||||
|
val jsonBytes = json.toString().toByteArray(Utf8)
|
||||||
|
|
||||||
|
assertThrows(FormatException::class.java) {
|
||||||
|
decoder.decode(jsonBytes, metadata.version, metadata.token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.stevesoltys.backup.metadata
|
||||||
|
|
||||||
|
import com.stevesoltys.backup.crypto.Crypto
|
||||||
|
import com.stevesoltys.backup.getRandomString
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@TestInstance(PER_CLASS)
|
||||||
|
internal class MetadataWriterDecoderTest {
|
||||||
|
|
||||||
|
private val crypto = mockk<Crypto>()
|
||||||
|
|
||||||
|
private val encoder = MetadataWriterImpl(crypto)
|
||||||
|
private val decoder = MetadataDecoderImpl()
|
||||||
|
|
||||||
|
private val metadata = BackupMetadata(
|
||||||
|
version = Random.nextBytes(1)[0],
|
||||||
|
token = Random.nextLong(),
|
||||||
|
androidVersion = Random.nextInt(),
|
||||||
|
deviceName = getRandomString()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `encoded metadata matches decoded metadata`() {
|
||||||
|
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.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.*
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
|
@ -32,6 +33,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
private val headerWriter = HeaderWriterImpl()
|
private val headerWriter = HeaderWriterImpl()
|
||||||
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 backupPlugin = mockk<BackupPlugin>()
|
private val backupPlugin = mockk<BackupPlugin>()
|
||||||
private val kvBackupPlugin = mockk<KVBackupPlugin>()
|
private val kvBackupPlugin = mockk<KVBackupPlugin>()
|
||||||
|
@ -39,7 +41,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
private val fullBackupPlugin = mockk<FullBackupPlugin>()
|
private val fullBackupPlugin = mockk<FullBackupPlugin>()
|
||||||
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
|
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
|
||||||
private val notificationManager = mockk<BackupNotificationManager>()
|
private val notificationManager = mockk<BackupNotificationManager>()
|
||||||
private val backup = BackupCoordinator(backupPlugin, kvBackup, fullBackup, notificationManager)
|
private val backup = BackupCoordinator(backupPlugin, kvBackup, fullBackup, metadataWriter, notificationManager)
|
||||||
|
|
||||||
private val restorePlugin = mockk<RestorePlugin>()
|
private val restorePlugin = mockk<RestorePlugin>()
|
||||||
private val kvRestorePlugin = mockk<KVRestorePlugin>()
|
private val kvRestorePlugin = mockk<KVRestorePlugin>()
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.stevesoltys.backup.transport.backup
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||||
import com.stevesoltys.backup.BackupNotificationManager
|
import com.stevesoltys.backup.BackupNotificationManager
|
||||||
|
import com.stevesoltys.backup.metadata.MetadataWriter
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
|
@ -11,6 +12,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertThrows
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
internal class BackupCoordinatorTest: BackupTest() {
|
internal class BackupCoordinatorTest: BackupTest() {
|
||||||
|
@ -18,13 +20,17 @@ internal class BackupCoordinatorTest: BackupTest() {
|
||||||
private val plugin = mockk<BackupPlugin>()
|
private val plugin = mockk<BackupPlugin>()
|
||||||
private val kv = mockk<KVBackup>()
|
private val kv = mockk<KVBackup>()
|
||||||
private val full = mockk<FullBackup>()
|
private val full = mockk<FullBackup>()
|
||||||
|
private val metadataWriter = mockk<MetadataWriter>()
|
||||||
private val notificationManager = mockk<BackupNotificationManager>()
|
private val notificationManager = mockk<BackupNotificationManager>()
|
||||||
|
|
||||||
private val backup = BackupCoordinator(plugin, kv, full, notificationManager)
|
private val backup = BackupCoordinator(plugin, kv, full, metadataWriter, notificationManager)
|
||||||
|
|
||||||
|
private val metadataOutputStream = mockk<OutputStream>()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `device initialization succeeds and delegates to plugin`() {
|
fun `device initialization succeeds and delegates to plugin`() {
|
||||||
every { plugin.initializeDevice() } just Runs
|
every { plugin.initializeDevice() } just Runs
|
||||||
|
expectWritingMetadata()
|
||||||
every { kv.hasState() } returns false
|
every { kv.hasState() } returns false
|
||||||
every { full.hasState() } returns false
|
every { full.hasState() } returns false
|
||||||
|
|
||||||
|
@ -110,4 +116,9 @@ internal class BackupCoordinatorTest: BackupTest() {
|
||||||
assertEquals(result, backup.finishBackup())
|
assertEquals(result, backup.finishBackup())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun expectWritingMetadata() {
|
||||||
|
every { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||||
|
every { metadataWriter.write(metadataOutputStream) } just Runs
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue