med-det-samme/app/src/androidTest/java/no/naiv/meddetsamme/backup/CloudBackupRoundTripTest.kt
Ole-Morten Duesund 8e8156488a Test backup: run every configured mechanism, stop misreporting passphrase-only as unconfigured
The user's report: with only a backup passphrase set (no S3), 'Test
backup nå' errored 'ikke konfigurert' even though Google Auto Backup
works on the passphrase alone. New runConfiguredBackups() writes the
Google blob whenever a passphrase exists and uploads to S3 only when
those credentials are present, throwing only when NOTHING is set up.
The toast now names what actually ran ('Kryptert Google-kopi
oppdatert' and/or the bucket key). Two instrumented tests pin both
ends: passphrase-only succeeds, nothing-configured throws.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:20:16 +02:00

120 lines
5.4 KiB
Kotlin

package no.naiv.meddetsamme.backup
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import no.naiv.meddetsamme.data.InventoryItem
import no.naiv.meddetsamme.data.MedDatabase
import no.naiv.meddetsamme.data.MedForm
import no.naiv.meddetsamme.data.Medication
import no.naiv.meddetsamme.settings.SettingsStore
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
/**
* The Google Auto Backup path end to end, without the OS backup transport
* (which UI/automation can't drive deterministically): writeCloudBlob encrypts
* with the stored passphrase, pendingCloudRestore fires only on an empty DB,
* and import() with the right passphrase brings the data back. This is the
* "new phone" recovery, and it must keep working — Keystore won't migrate, so
* the typed passphrase is the only key.
*/
@RunWith(AndroidJUnit4::class)
class CloudBackupRoundTripTest {
private val context = ApplicationProvider.getApplicationContext<android.content.Context>()
private val db get() = MedDatabase.get(context)
private val manager = BackupManager(context)
@Before fun clear() {
runBlocking { db.restoreAll(emptyList(), emptyList(), emptyList(), emptyList()) }
BackupManager.cloudBlobFile(context).delete()
}
@After fun cleanup() {
runBlocking { db.restoreAll(emptyList(), emptyList(), emptyList(), emptyList()) }
BackupManager.cloudBlobFile(context).delete()
SettingsStore(context).autoBackupPassphrase = null
}
@Test fun writeThenRestoreOnEmptyDb() = runBlocking {
SettingsStore(context).autoBackupPassphrase = "korrekt hest"
val itemId = db.inventoryDao().insert(
InventoryItem(name = "Levaxin", strength = "50 µg", unit = "tablett", form = MedForm.TABLET, stockUnits = 42.0),
)
db.medicationDao().insert(Medication(itemId = itemId, notes = "fastende"))
manager.writeCloudBlob()
val blob = BackupManager.cloudBlobFile(context)
assertTrue("blob written", blob.exists())
assertTrue("blob is an age v1 file", blob.readBytes().decodeToString(0, 21) == "age-encryption.org/v1")
// Non-empty DB → no restore offered.
assertNull(manager.pendingCloudRestore())
// Simulate the new device: empty DB, blob restored by the OS.
db.restoreAll(emptyList(), emptyList(), emptyList(), emptyList())
val pending = manager.pendingCloudRestore()
assertNotNull("restore offered on empty DB", pending)
val count = manager.import(pending!!.readBytes(), "korrekt hest".toCharArray())
assertEquals(1, count)
assertEquals("Levaxin", db.inventoryDao().getAll().single().name)
assertEquals("fastende", db.medicationDao().getAllWithItems().single().med.notes)
}
@Test fun wrongPassphraseFailsAndKeepsDbEmpty() = runBlocking {
SettingsStore(context).autoBackupPassphrase = "riktig"
val itemId = db.inventoryDao().insert(
InventoryItem(name = "Levaxin", strength = "50 µg", unit = "tablett", form = MedForm.TABLET, stockUnits = 42.0),
)
db.medicationDao().insert(Medication(itemId = itemId))
manager.writeCloudBlob()
db.restoreAll(emptyList(), emptyList(), emptyList(), emptyList())
val blob = manager.pendingCloudRestore()!!
try {
manager.import(blob.readBytes(), "feil".toCharArray())
org.junit.Assert.fail("wrong passphrase must throw")
} catch (_: Exception) { /* expected */ }
assertTrue("DB still empty after failed restore", db.inventoryDao().getAll().isEmpty())
}
@Test fun testBackupWithOnlyPassphraseSucceeds() = runBlocking {
// The user's report: passphrase set, no S3 → "Test backup" must NOT
// error with "not configured"; it writes the Google blob and says so.
SettingsStore(context).autoBackupPassphrase = "x"
db.inventoryDao().insert(InventoryItem(name = "A", strength = "", unit = "stk", form = MedForm.OTHER))
val outcome = manager.runConfiguredBackups()
assertTrue("cloud blob written", outcome.cloudBlobWritten)
assertNull("no S3 attempted when unconfigured", outcome.s3Key)
assertTrue(BackupManager.cloudBlobFile(context).exists())
}
@Test fun testBackupWithNothingConfiguredThrows() = runBlocking {
SettingsStore(context).autoBackupPassphrase = null
try {
manager.runConfiguredBackups()
org.junit.Assert.fail("must require at least one mechanism")
} catch (_: IllegalArgumentException) { /* expected */ }
}
@Test fun noPassphraseRemovesBlob() = runBlocking {
SettingsStore(context).autoBackupPassphrase = "x"
db.inventoryDao().insert(InventoryItem(name = "A", strength = "", unit = "stk", form = MedForm.OTHER))
manager.writeCloudBlob()
assertTrue(BackupManager.cloudBlobFile(context).exists())
// Clearing the passphrase must stop offering data to Google.
SettingsStore(context).autoBackupPassphrase = null
manager.writeCloudBlob()
assertTrue("blob removed when passphrase cleared", !BackupManager.cloudBlobFile(context).exists())
}
}