Add import/export tests for backup of recipients and threads.

This commit is contained in:
Clark
2024-02-29 12:04:23 -05:00
committed by Alex Hart
parent 5740b768d0
commit 32fe927bfc
6 changed files with 421 additions and 30 deletions

View File

@@ -15,21 +15,32 @@ import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.proto.Call
import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.Contact
import org.thoughtcrime.securesms.backup.v2.proto.DistributionList
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.proto.Group
import org.thoughtcrime.securesms.backup.v2.proto.Recipient
import org.thoughtcrime.securesms.backup.v2.proto.ReleaseNotes
import org.thoughtcrime.securesms.backup.v2.proto.Self
import org.thoughtcrime.securesms.backup.v2.proto.StickerPack
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import org.whispersystems.signalservice.api.util.toByteArray
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.ArrayList
import java.util.UUID
import kotlin.random.Random
import kotlin.time.Duration.Companion.days
/**
* Test the import and export of message backup frames to make sure what
* goes in, comes out.
*/
class ImportExportTest {
companion object {
val SELF_ACI = ServiceId.ACI.from(UUID.fromString("77770000-b477-4f35-a824-d92987a63641"))
@@ -39,6 +50,7 @@ class ImportExportTest {
val defaultBackupInfo = BackupInfo(version = 1L, backupTimeMs = 123456L)
val selfRecipient = Recipient(id = 1, self = Self())
val releaseNotes = Recipient(id = 2, releaseNotes = ReleaseNotes())
val standardAccountData = AccountData(
profileKey = SELF_PROFILE_KEY.serialize().toByteString(),
username = "testusername",
@@ -69,6 +81,11 @@ class ImportExportTest {
preferredReactionEmoji = listOf("a", "b", "c")
)
)
/**
* When using standardFrames you must start recipient ids at 3.
*/
private val standardFrames = arrayOf(defaultBackupInfo, standardAccountData, selfRecipient, releaseNotes)
}
@Before
@@ -82,13 +99,290 @@ class ImportExportTest {
@Test
fun accountAndSelf() {
importExport(*standardFrames)
}
@Test
fun individualRecipients() {
importExport(
defaultBackupInfo,
standardAccountData,
selfRecipient
*standardFrames,
Recipient(
id = 3,
contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(),
pni = TestRecipientUtils.nextPni().toByteString(),
username = "coolusername",
e164 = 141255501234,
blocked = true,
hidden = true,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Alexa",
profileFamilyName = "Kim",
hideStory = true
)
),
Recipient(
id = 4,
contact = Contact(
aci = null,
pni = null,
username = null,
e164 = 141255501235,
blocked = true,
hidden = true,
registered = Contact.Registered.NOT_REGISTERED,
unregisteredTimestamp = 1234568927398L,
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = false,
profileGivenName = "Peter",
profileFamilyName = "Kim",
hideStory = true
)
)
)
}
@Test
fun groupRecipients() {
importExport(
*standardFrames,
Recipient(
id = 3,
group = Group(
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
whitelisted = true,
hideStory = true,
storySendMode = Group.StorySendMode.ENABLED,
name = "Cool test group"
)
),
Recipient(
id = 4,
group = Group(
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
whitelisted = false,
hideStory = false,
storySendMode = Group.StorySendMode.DEFAULT,
name = "Cool test group"
)
)
)
}
@Test
fun distributionListRecipients() {
importExport(
*standardFrames,
Recipient(
id = 3,
contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(),
pni = TestRecipientUtils.nextPni().toByteString(),
username = "coolusername",
e164 = 141255501234,
blocked = true,
hidden = true,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Alexa",
profileFamilyName = "Kim",
hideStory = true
)
),
Recipient(
id = 4,
contact = Contact(
aci = null,
pni = null,
username = null,
e164 = 141255501235,
blocked = true,
hidden = true,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Peter",
profileFamilyName = "Kim",
hideStory = true
)
),
Recipient(
id = 5,
contact = Contact(
aci = null,
pni = null,
username = null,
e164 = 141255501236,
blocked = true,
hidden = true,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Father",
profileFamilyName = "Kim",
hideStory = true
)
),
Recipient(
id = 6,
distributionList = DistributionList(
name = "Kim Family",
distributionId = DistributionId.create().asUuid().toByteArray().toByteString(),
allowReplies = true,
deletionTimestamp = 0L,
privacyMode = DistributionList.PrivacyMode.ONLY_WITH,
memberRecipientIds = listOf(3, 4, 5)
)
)
)
}
@Test
fun deletedDistributionList() {
val alexa = Recipient(
id = 3,
contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(),
pni = TestRecipientUtils.nextPni().toByteString(),
username = "coolusername",
e164 = 141255501234,
blocked = true,
hidden = true,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Alexa",
profileFamilyName = "Kim",
hideStory = true
)
)
import(
*standardFrames,
alexa,
Recipient(
id = 6,
distributionList = DistributionList(
name = "Deleted list",
distributionId = DistributionId.create().asUuid().toByteArray().toByteString(),
allowReplies = true,
deletionTimestamp = 12345L,
privacyMode = DistributionList.PrivacyMode.ONLY_WITH,
memberRecipientIds = listOf(3)
)
)
)
val exported = export()
val expected = exportFrames(
*standardFrames,
alexa
)
compare(expected, exported)
}
@Test
fun chatThreads() {
importExport(
*standardFrames,
Recipient(
id = 3,
contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(),
pni = TestRecipientUtils.nextPni().toByteString(),
username = "coolusername",
e164 = 141255501234,
blocked = false,
hidden = false,
registered = Contact.Registered.REGISTERED,
unregisteredTimestamp = 0L,
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
profileSharing = true,
profileGivenName = "Alexa",
profileFamilyName = "Kim",
hideStory = true
)
),
Recipient(
id = 4,
group = Group(
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
whitelisted = true,
hideStory = true,
storySendMode = Group.StorySendMode.DEFAULT,
name = "Cool test group"
)
),
Chat(
id = 1,
recipientId = 3,
archived = true,
pinnedOrder = 1,
expirationTimerMs = 1.days.inWholeMilliseconds,
muteUntilMs = System.currentTimeMillis(),
markedUnread = true,
dontNotifyForMentionsIfMuted = true,
wallpaper = null
)
)
}
/**
* Export passed in frames as a backup. Does not automatically include
* any standard frames (e.g. backup header).
*/
private fun exportFrames(vararg objects: Any): ByteArray {
val outputStream = ByteArrayOutputStream()
val writer = EncryptedBackupWriter(
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
aci = SignalStore.account().aci!!,
outputStream = outputStream,
append = { mac -> outputStream.write(mac) }
)
writer.use {
for (obj in objects) {
when (obj) {
is BackupInfo -> writer.write(obj)
is AccountData -> writer.write(Frame(account = obj))
is Recipient -> writer.write(Frame(recipient = obj))
is Chat -> writer.write(Frame(chat = obj))
is ChatItem -> writer.write(Frame(chatItem = obj))
is Call -> writer.write(Frame(call = obj))
is StickerPack -> writer.write(Frame(stickerPack = obj))
else -> Assert.fail("invalid object $obj")
}
}
}
return outputStream.toByteArray()
}
/**
* Exports the passed in frames as a backup and then attempts to
* import them.
*/
private fun import(vararg objects: Any) {
val importData = exportFrames(*objects)
BackupRepository.import(length = importData.size.toLong(), inputStreamFactory = { ByteArrayInputStream(importData) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
}
/**
* Export our current database as a backup.
*/
private fun export() = BackupRepository.export()
/**
* Imports the passed in frames and then exports them.
*
* It will do a comparison to assert that the import and export
* are equal.
*/
private fun importExport(vararg objects: Any) {
val outputStream = ByteArrayOutputStream()
val writer = EncryptedBackupWriter(
@@ -115,7 +409,7 @@ class ImportExportTest {
val importData = outputStream.toByteArray()
BackupRepository.import(length = importData.size.toLong(), inputStreamFactory = { ByteArrayInputStream(importData) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
val export = BackupRepository.export()
val export = export()
compare(importData, export)
}
@@ -180,6 +474,18 @@ class ImportExportTest {
}
private fun <T, R : Comparable<R>> prettyAssertEquals(import: List<T>, export: List<T>, selector: (T) -> R?) {
if (import.size != export.size) {
var msg = StringBuilder()
for (i in import) {
msg.append(i)
msg.append("\n")
}
for (i in export) {
msg.append(i)
msg.append("\n")
}
Assert.fail(msg.toString())
}
Assert.assertEquals(import.size, export.size)
val sortedImport = import.sortedBy(selector)
val sortedExport = export.sortedBy(selector)

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.whispersystems.signalservice.api.util.toByteArray
import java.util.UUID
import kotlin.random.Random
object TestRecipientUtils {
private var upperGenAci = 13131313L
private var lowerGenAci = 0L
private var upperGenPni = 12121212L
private var lowerGenPni = 0L
private var groupMasterKeyRandom = Random(12345)
fun generateProfileKey(): ByteArray {
return ProfileKeyUtil.createNew().serialize()
}
fun nextPni(): ByteArray {
synchronized(this) {
lowerGenPni++
var uuid = UUID(upperGenPni, lowerGenPni)
return uuid.toByteArray()
}
}
fun nextAci(): ByteArray {
synchronized(this) {
lowerGenAci++
var uuid = UUID(upperGenAci, lowerGenAci)
return uuid.toByteArray()
}
}
fun generateGroupMasterKey(): ByteArray {
val masterKey = ByteArray(32)
groupMasterKeyRandom.nextBytes(masterKey)
return masterKey
}
}