mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-14 12:47:36 +01:00
Add story and call link age-off delete syncing support.
This commit is contained in:
@@ -4,6 +4,7 @@ import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Serializer
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.delete
|
||||
@@ -345,6 +346,50 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes storageIds from call links in [RecipientTable] that were deleted before [deletedBefore].
|
||||
*/
|
||||
fun removeStorageIdsFromOldDeletedCallLinks(deletedBefore: Long): Int {
|
||||
return writableDatabase
|
||||
.update(RecipientTable.TABLE_NAME)
|
||||
.values(RecipientTable.STORAGE_SERVICE_ID to null)
|
||||
.where(
|
||||
"""
|
||||
${RecipientTable.STORAGE_SERVICE_ID} NOT NULL AND ${RecipientTable.ID} IN (
|
||||
SELECT $RECIPIENT_ID
|
||||
FROM $TABLE_NAME
|
||||
WHERE $DELETION_TIMESTAMP > 0 AND $DELETION_TIMESTAMP < ?
|
||||
)
|
||||
""",
|
||||
deletedBefore
|
||||
)
|
||||
.run()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes storageIds of deleted call links whose storageIds are in the given collection.
|
||||
*/
|
||||
fun removeStorageIdsFromLocalOnlyDeletedCallLinks(storageIds: Collection<StorageId>): Int {
|
||||
val values = contentValuesOf(RecipientTable.STORAGE_SERVICE_ID to null)
|
||||
var updated = 0
|
||||
|
||||
SqlUtil.buildCollectionQuery(
|
||||
RecipientTable.STORAGE_SERVICE_ID,
|
||||
storageIds.map { Base64.encodeWithPadding(it.raw) },
|
||||
"""
|
||||
${RecipientTable.ID} IN (
|
||||
SELECT $RECIPIENT_ID
|
||||
FROM $TABLE_NAME
|
||||
WHERE $DELETION_TIMESTAMP > 0
|
||||
) AND
|
||||
"""
|
||||
).forEach {
|
||||
updated += writableDatabase.update(RecipientTable.TABLE_NAME, values, it.where, it.whereArgs)
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
fun deleteNonAdminCallLinks(roomIds: Set<CallLinkRoomId>) {
|
||||
val queries = SqlUtil.buildCollectionQuery(ROOM_ID, roomIds.map { it.serialize() })
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.thoughtcrime.securesms.database.ThreadTable.Companion.ID
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.storage.SignalChatFolderRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord as RemoteChatFolderRecord
|
||||
@@ -545,13 +544,13 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes storageIds from folders that have been deleted for [RemoteConfig.messageQueueTime].
|
||||
* Removes storageIds from folders that were deleted before [deletedBefore]. Callers derive that cutoff from the message queue time.
|
||||
*/
|
||||
fun removeStorageIdsFromOldDeletedFolders(now: Long): Int {
|
||||
fun removeStorageIdsFromOldDeletedFolders(deletedBefore: Long): Int {
|
||||
return writableDatabase
|
||||
.update(ChatFolderTable.TABLE_NAME)
|
||||
.values(ChatFolderTable.STORAGE_SERVICE_ID to null)
|
||||
.where("${ChatFolderTable.STORAGE_SERVICE_ID} NOT NULL AND ${ChatFolderTable.DELETED_TIMESTAMP_MS} > 0 AND ${ChatFolderTable.DELETED_TIMESTAMP_MS} < ?", now - RemoteConfig.messageQueueTime)
|
||||
.where("${ChatFolderTable.STORAGE_SERVICE_ID} NOT NULL AND ${ChatFolderTable.DELETED_TIMESTAMP_MS} > 0 AND ${ChatFolderTable.DELETED_TIMESTAMP_MS} < ?", deletedBefore)
|
||||
.run()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireObject
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyData
|
||||
@@ -28,6 +29,7 @@ import org.thoughtcrime.securesms.storage.StorageRecordUpdate
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.api.storage.recipientServiceAddresses
|
||||
import java.util.UUID
|
||||
|
||||
@@ -582,6 +584,54 @@ class DistributionListTables constructor(context: Context?, databaseHelper: Sign
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes storageIds from distribution lists in [RecipientTable] that were deleted before [deletedBefore].
|
||||
*
|
||||
* Never touches "My Story".
|
||||
*/
|
||||
fun removeStorageIdsFromOldDeletedLists(deletedBefore: Long): Int {
|
||||
return writableDatabase
|
||||
.update(RecipientTable.TABLE_NAME)
|
||||
.values(RecipientTable.STORAGE_SERVICE_ID to null)
|
||||
.where(
|
||||
"""
|
||||
${RecipientTable.STORAGE_SERVICE_ID} NOT NULL AND ${RecipientTable.ID} IN (
|
||||
SELECT ${ListTable.RECIPIENT_ID}
|
||||
FROM ${ListTable.TABLE_NAME}
|
||||
WHERE ${ListTable.TABLE_NAME}.${ListTable.ID} != ${DistributionListId.MY_STORY_ID} AND ${ListTable.DELETION_TIMESTAMP} > 0 AND ${ListTable.DELETION_TIMESTAMP} < ?
|
||||
)
|
||||
""",
|
||||
deletedBefore
|
||||
)
|
||||
.run()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes storageIds of deleted distribution lists whose storageIds are in the given collection.
|
||||
*
|
||||
* Never touches "My Story".
|
||||
*/
|
||||
fun removeStorageIdsFromLocalOnlyDeletedLists(storageIds: Collection<StorageId>): Int {
|
||||
val values = contentValuesOf(RecipientTable.STORAGE_SERVICE_ID to null)
|
||||
var updated = 0
|
||||
|
||||
SqlUtil.buildCollectionQuery(
|
||||
RecipientTable.STORAGE_SERVICE_ID,
|
||||
storageIds.map { Base64.encodeWithPadding(it.raw) },
|
||||
"""
|
||||
${RecipientTable.ID} IN (
|
||||
SELECT ${ListTable.RECIPIENT_ID}
|
||||
FROM ${ListTable.TABLE_NAME}
|
||||
WHERE ${ListTable.TABLE_NAME}.${ListTable.ID} != ${DistributionListId.MY_STORY_ID} AND ${ListTable.DELETION_TIMESTAMP} > 0
|
||||
) AND
|
||||
"""
|
||||
).forEach {
|
||||
updated += writableDatabase.update(RecipientTable.TABLE_NAME, values, it.where, it.whereArgs)
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
fun getRecipientIdForSyncRecord(record: SignalStoryDistributionListRecord): RecipientId? {
|
||||
val uuid: UUID = requireNotNull(UuidUtil.parseOrNull(record.proto.identifier)) { "Incoming record did not have a valid identifier." }
|
||||
val distributionId = DistributionId.from(uuid)
|
||||
|
||||
@@ -36,7 +36,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels.toLocal
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.storage.SignalNotificationProfileRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import java.time.DayOfWeek
|
||||
@@ -492,13 +491,13 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes storageIds from notification profiles that have been deleted for [RemoteConfig.messageQueueTime].
|
||||
* Removes storageIds from notification profiles that were deleted before [deletedBefore].
|
||||
*/
|
||||
fun removeStorageIdsFromOldDeletedProfiles(now: Long): Int {
|
||||
fun removeStorageIdsFromOldDeletedProfiles(deletedBefore: Long): Int {
|
||||
return writableDatabase
|
||||
.update(NotificationProfileTable.TABLE_NAME)
|
||||
.values(NotificationProfileTable.STORAGE_SERVICE_ID to null)
|
||||
.where("${NotificationProfileTable.STORAGE_SERVICE_ID} NOT NULL AND ${NotificationProfileTable.DELETED_TIMESTAMP_MS} > 0 AND ${NotificationProfileTable.DELETED_TIMESTAMP_MS} < ?", now - RemoteConfig.messageQueueTime)
|
||||
.where("${NotificationProfileTable.STORAGE_SERVICE_ID} NOT NULL AND ${NotificationProfileTable.DELETED_TIMESTAMP_MS} > 0 AND ${NotificationProfileTable.DELETED_TIMESTAMP_MS} < ?", deletedBefore)
|
||||
.run()
|
||||
}
|
||||
|
||||
|
||||
@@ -1144,18 +1144,18 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes storageIds from unregistered recipients who were unregistered more than [RemoteConfig.messageQueueTime] ago.
|
||||
* Removes storageIds from unregistered recipients who were unregistered before [unregisteredBefore].
|
||||
*
|
||||
* Never touches self: our own storageId backs the ACCOUNT record, so it always needs to be present. If self ever ends up with a stale
|
||||
* [UNREGISTERED_TIMESTAMP], clearing it here would leave us regenerating our storageId on every single storage sync.
|
||||
*
|
||||
* @return The number of rows affected.
|
||||
*/
|
||||
fun removeStorageIdsFromOldUnregisteredRecipients(now: Long): Int {
|
||||
fun removeStorageIdsFromOldUnregisteredRecipients(unregisteredBefore: Long): Int {
|
||||
return writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(STORAGE_SERVICE_ID to null)
|
||||
.where("$STORAGE_SERVICE_ID NOT NULL AND $ID != ${Recipient.self().id.toLong()} AND $UNREGISTERED_TIMESTAMP > 0 AND $UNREGISTERED_TIMESTAMP < ?", now - RemoteConfig.messageQueueTime)
|
||||
.where("$STORAGE_SERVICE_ID NOT NULL AND $ID != ${Recipient.self().id.toLong()} AND $UNREGISTERED_TIMESTAMP > 0 AND $UNREGISTERED_TIMESTAMP < ?", unregisteredBefore)
|
||||
.run()
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ import org.thoughtcrime.securesms.stickers.BlessedPacks
|
||||
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.storage.SignalStickerPackRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import java.io.Closeable
|
||||
@@ -573,13 +572,13 @@ class StickerTables(
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes storage ids from packs that have been deleted for longer than the message queue time.
|
||||
* Removes storage ids from packs that were deleted before [deletedBefore].
|
||||
*/
|
||||
fun removeStorageIdsFromOldDeletedPacks(now: Long): Int {
|
||||
fun removeStorageIdsFromOldDeletedPacks(deletedBefore: Long): Int {
|
||||
return writableDatabase
|
||||
.update(Pack.TABLE_NAME)
|
||||
.values(Pack.STORAGE_SERVICE_ID to null)
|
||||
.where("${Pack.STORAGE_SERVICE_ID} NOT NULL AND ${Pack.DELETED_TIMESTAMP_MS} > 0 AND ${Pack.DELETED_TIMESTAMP_MS} < ?", now - RemoteConfig.messageQueueTime)
|
||||
.where("${Pack.STORAGE_SERVICE_ID} NOT NULL AND ${Pack.DELETED_TIMESTAMP_MS} > 0 AND ${Pack.DELETED_TIMESTAMP_MS} < ?", deletedBefore)
|
||||
.run()
|
||||
}
|
||||
|
||||
|
||||
@@ -307,9 +307,11 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
|
||||
val updatedFolders = SignalDatabase.chatFolders.removeStorageIdsFromLocalOnlyDeletedFolders(idDifference.localOnlyIds)
|
||||
val updatedProfiles = SignalDatabase.notificationProfiles.removeStorageIdsFromLocalOnlyDeletedProfiles(idDifference.localOnlyIds)
|
||||
val updatedPacks = SignalDatabase.stickers.removeStorageIdsFromLocalOnlyDeletedPacks(idDifference.localOnlyIds)
|
||||
val updatedLists = SignalDatabase.distributionLists.removeStorageIdsFromLocalOnlyDeletedLists(idDifference.localOnlyIds)
|
||||
val updatedCallLinks = SignalDatabase.callLinks.removeStorageIdsFromLocalOnlyDeletedCallLinks(idDifference.localOnlyIds)
|
||||
|
||||
if (updatedRecipients > 0 || updatedFolders > 0 || updatedProfiles > 0 || updatedPacks > 0) {
|
||||
Log.w(TAG, "Found $updatedRecipients recipients, $updatedFolders folders, $updatedProfiles notification profiles, $updatedPacks sticker packs that were deleted remotely but only marked unregistered/deleted locally. Removed those from local store. Recalculating diff.")
|
||||
if (updatedRecipients > 0 || updatedFolders > 0 || updatedProfiles > 0 || updatedPacks > 0 || updatedLists > 0 || updatedCallLinks > 0) {
|
||||
Log.w(TAG, "Found $updatedRecipients recipients, $updatedFolders folders, $updatedProfiles notification profiles, $updatedPacks sticker packs, $updatedLists distribution lists, $updatedCallLinks call links that were deleted remotely but only marked unregistered/deleted locally. Removed those from local store. Recalculating diff.")
|
||||
|
||||
localStorageIdsBeforeMerge = getAllLocalStorageIds(self)
|
||||
idDifference = StorageSyncHelper.findIdDifference(remoteManifest.storageIds, localStorageIdsBeforeMerge)
|
||||
@@ -401,12 +403,16 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
|
||||
stopwatch.split("known-unknowns")
|
||||
|
||||
val remoteWriteOperation: WriteOperationResult = db.withinTransaction {
|
||||
val removedUnregistered = SignalDatabase.recipients.removeStorageIdsFromOldUnregisteredRecipients(System.currentTimeMillis())
|
||||
val removedDeletedFolders = SignalDatabase.chatFolders.removeStorageIdsFromOldDeletedFolders(System.currentTimeMillis())
|
||||
val removedDeletedProfiles = SignalDatabase.notificationProfiles.removeStorageIdsFromOldDeletedProfiles(System.currentTimeMillis())
|
||||
val removedDeletedPacks = SignalDatabase.stickers.removeStorageIdsFromOldDeletedPacks(System.currentTimeMillis())
|
||||
if (removedUnregistered > 0 || removedDeletedFolders > 0 || removedDeletedProfiles > 0 || removedDeletedPacks > 0) {
|
||||
Log.i(TAG, "Removed $removedUnregistered unregistered, $removedDeletedFolders folders, $removedDeletedProfiles notification profiles, $removedDeletedPacks sticker packs from storage service that have been deleted for longer than ${RemoteConfig.messageQueueTime.milliseconds.inWholeDays} days.")
|
||||
val expiredBefore = System.currentTimeMillis() - RemoteConfig.messageQueueTime
|
||||
val removedUnregistered = SignalDatabase.recipients.removeStorageIdsFromOldUnregisteredRecipients(expiredBefore)
|
||||
val removedDeletedFolders = SignalDatabase.chatFolders.removeStorageIdsFromOldDeletedFolders(expiredBefore)
|
||||
val removedDeletedProfiles = SignalDatabase.notificationProfiles.removeStorageIdsFromOldDeletedProfiles(expiredBefore)
|
||||
val removedDeletedPacks = SignalDatabase.stickers.removeStorageIdsFromOldDeletedPacks(expiredBefore)
|
||||
val removedDeletedLists = SignalDatabase.distributionLists.removeStorageIdsFromOldDeletedLists(expiredBefore)
|
||||
val removedDeletedCallLinks = SignalDatabase.callLinks.removeStorageIdsFromOldDeletedCallLinks(expiredBefore)
|
||||
|
||||
if (removedUnregistered > 0 || removedDeletedFolders > 0 || removedDeletedProfiles > 0 || removedDeletedPacks > 0 || removedDeletedLists > 0 || removedDeletedCallLinks > 0) {
|
||||
Log.i(TAG, "Removed $removedUnregistered unregistered, $removedDeletedFolders folders, $removedDeletedProfiles notification profiles, $removedDeletedPacks sticker packs, $removedDeletedLists distribution lists, $removedDeletedCallLinks call links from storage service that have been deleted for longer than ${RemoteConfig.messageQueueTime.milliseconds.inWholeDays} days.")
|
||||
}
|
||||
|
||||
self = freshSelf()
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.toOptional
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.whispersystems.signalservice.api.storage.SignalCallLinkRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
@@ -50,12 +51,33 @@ class CallLinkRecordProcessor : DefaultStorageRecordProcessor<SignalCallLinkReco
|
||||
rootKey = callRootKey.keyBytes.toByteString()
|
||||
adminPasskey = callLink.credentials.adminPassBytes.toByteString()
|
||||
deletedAtTimestampMs = callLink.deletionTimestamp
|
||||
}.build().toSignalCallLinkRecord(StorageId.forCallLink(keyGenerator.generate())).toOptional()
|
||||
}.build().toSignalCallLinkRecord(localStorageId(callLink.recipientId, keyGenerator)).toOptional()
|
||||
} else {
|
||||
return Optional.empty<SignalCallLinkRecord>()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The storageId of a call link lives on its recipient. Returning the real one matters: [SignalCallLinkRecord] compares by id, so handing back a
|
||||
* generated id would make every processed record look changed and trigger a pointless local update.
|
||||
*
|
||||
* Generating one is defensive only, and in particular is not how an aged-off tombstone recovers. Deleting a call link clears its admin key, so
|
||||
* [getMatching] returns empty for that case and [insertLocal] restores the id from the remote record instead.
|
||||
*/
|
||||
private fun localStorageId(recipientId: RecipientId, keyGenerator: StorageKeyGenerator): StorageId {
|
||||
val existing = SignalDatabase.recipients.getRecordForSync(recipientId)?.storageId
|
||||
|
||||
if (existing != null) {
|
||||
return StorageId.forCallLink(existing)
|
||||
}
|
||||
|
||||
Log.w(TAG, "Call link was missing a storageId, generating one.")
|
||||
val generated = keyGenerator.generate()
|
||||
SignalDatabase.recipients.updateStorageId(recipientId, generated)
|
||||
|
||||
return StorageId.forCallLink(generated)
|
||||
}
|
||||
|
||||
/**
|
||||
* A deleted record takes precedence over a non-deleted record
|
||||
* An earlier deletion takes precedence over a later deletion
|
||||
|
||||
+6
-1
@@ -84,7 +84,12 @@ class StoryDistributionListRecordProcessor : DefaultStorageRecordProcessor<Signa
|
||||
throw InvalidGroupTypeException()
|
||||
}
|
||||
|
||||
return StorageSyncModels.localToRemoteRecord(recordForSync).let { it.proto.storyDistributionList!!.toSignalStoryDistributionListRecord(it.id) }.asOptional()
|
||||
val storageId = recordForSync.storageId ?: run {
|
||||
Log.w(TAG, "Distribution list was missing a storage id, generating one. It was likely aged off locally while the remote still had it.")
|
||||
keyGenerator.generate().also { SignalDatabase.recipients.updateStorageId(matching, it) }
|
||||
}
|
||||
|
||||
return StorageSyncModels.localToRemoteRecord(recordForSync, storageId).let { it.proto.storyDistributionList!!.toSignalStoryDistributionListRecord(it.id) }.asOptional()
|
||||
} else {
|
||||
Log.d(TAG, "Could not find a matching record. Returning an empty.")
|
||||
return Optional.empty()
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.app.Application
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.isNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
|
||||
import org.thoughtcrime.securesms.testutil.RecipientTestRule
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class CallLinkTableTest_storageIdAgeOff {
|
||||
|
||||
@get:Rule
|
||||
val recipients = RecipientTestRule()
|
||||
|
||||
@Test
|
||||
fun `given call links deleted long ago and recently, when I age off storage ids, then I expect only the old one to lose its storage id`() {
|
||||
val old = insertCallLink(rootKey = 1, deletedAt = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(46))
|
||||
val recent = insertCallLink(rootKey = 2, deletedAt = System.currentTimeMillis())
|
||||
|
||||
SignalDatabase.callLinks.removeStorageIdsFromOldDeletedCallLinks(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(45))
|
||||
|
||||
assertThat(storageIdOf(old)).isNull()
|
||||
assertThat(storageIdOf(recent)).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given an old deleted call link and a live call link, when I age off storage ids, then I expect only the deleted one to lose its storage id`() {
|
||||
val deleted = insertCallLink(rootKey = 1, deletedAt = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(46))
|
||||
val live = insertCallLink(rootKey = 2)
|
||||
|
||||
SignalDatabase.callLinks.removeStorageIdsFromOldDeletedCallLinks(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(45))
|
||||
|
||||
assertThat(storageIdOf(deleted)).isNull()
|
||||
assertThat(storageIdOf(live)).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a deleted call link and a live call link whose storage ids are local only, then I expect only the deleted one to lose its storage id`() {
|
||||
val deleted = insertCallLink(rootKey = 1, deletedAt = System.currentTimeMillis())
|
||||
val live = insertCallLink(rootKey = 2)
|
||||
|
||||
SignalDatabase.callLinks.removeStorageIdsFromLocalOnlyDeletedCallLinks(listOf(storageIdOf(deleted)!!, storageIdOf(live)!!))
|
||||
|
||||
assertThat(storageIdOf(deleted)).isNull()
|
||||
assertThat(storageIdOf(live)).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a call link deleted one minute ago, when its storage id is local only, then I expect it to lose its storage id`() {
|
||||
val deleted = insertCallLink(rootKey = 1, deletedAt = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(1))
|
||||
|
||||
SignalDatabase.callLinks.removeStorageIdsFromLocalOnlyDeletedCallLinks(listOf(storageIdOf(deleted)!!))
|
||||
|
||||
assertThat(storageIdOf(deleted)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given two deleted call links, when only one storage id is local only, then I expect only that one to lose its storage id`() {
|
||||
val localOnly = insertCallLink(rootKey = 1, deletedAt = System.currentTimeMillis())
|
||||
val stillRemote = insertCallLink(rootKey = 2, deletedAt = System.currentTimeMillis())
|
||||
|
||||
SignalDatabase.callLinks.removeStorageIdsFromLocalOnlyDeletedCallLinks(listOf(storageIdOf(localOnly)!!))
|
||||
|
||||
assertThat(storageIdOf(localOnly)).isNull()
|
||||
assertThat(storageIdOf(stillRemote)).isNotNull()
|
||||
}
|
||||
|
||||
private fun storageIdOf(recipientId: RecipientId): StorageId? {
|
||||
return SignalDatabase.recipients.getContactStorageSyncIdsMap()[recipientId]
|
||||
}
|
||||
|
||||
private fun insertCallLink(rootKey: Int, deletedAt: Long = 0L): RecipientId {
|
||||
val rootKeyBytes = ByteArray(16) { rootKey.toByte() }
|
||||
|
||||
return SignalDatabase.callLinks.insertCallLink(
|
||||
callLink = CallLinkTable.CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = CallLinkRoomId.fromBytes(rootKeyBytes),
|
||||
credentials = CallLinkCredentials(
|
||||
linkKeyBytes = rootKeyBytes,
|
||||
adminPassBytes = if (deletedAt > 0) null else ByteArray(16) { 9 }
|
||||
),
|
||||
state = SignalCallLinkState(),
|
||||
deletionTimestamp = deletedAt
|
||||
),
|
||||
deletionTimestamp = deletedAt
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.app.Application
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.isNull
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
@@ -9,12 +12,15 @@ import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.signal.core.models.ServiceId.ACI
|
||||
import org.signal.core.util.update
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testutil.RecipientTestRule
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
@@ -86,6 +92,83 @@ class DistributionListTablesTest {
|
||||
Assert.fail("Expected an assertion error.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given lists deleted long ago and recently, when I age off storage ids, then I expect only the old one to lose its storage id`() {
|
||||
val old = insertList("old", deletedAt = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(46))
|
||||
val recent = insertList("recent", deletedAt = System.currentTimeMillis())
|
||||
|
||||
distributionDatabase.removeStorageIdsFromOldDeletedLists(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(45))
|
||||
|
||||
assertThat(storageIdOf(old)).isNull()
|
||||
assertThat(storageIdOf(recent)).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given an old deleted list and a live list, when I age off storage ids, then I expect only the deleted one to lose its storage id`() {
|
||||
val deleted = insertList("deleted", deletedAt = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(46))
|
||||
val live = insertList("live")
|
||||
|
||||
distributionDatabase.removeStorageIdsFromOldDeletedLists(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(45))
|
||||
|
||||
assertThat(storageIdOf(deleted)).isNull()
|
||||
assertThat(storageIdOf(live)).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given My Story is somehow tombstoned, when I age off storage ids, then I expect it to keep its storage id`() {
|
||||
val deleted = insertList("deleted", deletedAt = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(46))
|
||||
val myStory = distributionDatabase.getRecipientId(DistributionListId.MY_STORY)!!
|
||||
setDeletionTimestamp(DistributionListId.MY_STORY, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(46))
|
||||
|
||||
distributionDatabase.removeStorageIdsFromOldDeletedLists(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(45))
|
||||
|
||||
assertThat(storageIdOf(deleted)).isNull()
|
||||
assertThat(storageIdOf(myStory)).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a deleted list and a live list whose storage ids are local only, then I expect only the deleted one to lose its storage id`() {
|
||||
val deleted = insertList("deleted", deletedAt = System.currentTimeMillis())
|
||||
val live = insertList("live")
|
||||
|
||||
distributionDatabase.removeStorageIdsFromLocalOnlyDeletedLists(listOf(storageIdOf(deleted)!!, storageIdOf(live)!!))
|
||||
|
||||
assertThat(storageIdOf(deleted)).isNull()
|
||||
assertThat(storageIdOf(live)).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a list deleted one minute ago, when its storage id is local only, then I expect it to lose its storage id`() {
|
||||
val deleted = insertList("deleted", deletedAt = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(1))
|
||||
|
||||
distributionDatabase.removeStorageIdsFromLocalOnlyDeletedLists(listOf(storageIdOf(deleted)!!))
|
||||
|
||||
assertThat(storageIdOf(deleted)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given two deleted lists, when only one storage id is local only, then I expect only that one to lose its storage id`() {
|
||||
val localOnly = insertList("localOnly", deletedAt = System.currentTimeMillis())
|
||||
val stillRemote = insertList("stillRemote", deletedAt = System.currentTimeMillis())
|
||||
|
||||
distributionDatabase.removeStorageIdsFromLocalOnlyDeletedLists(listOf(storageIdOf(localOnly)!!))
|
||||
|
||||
assertThat(storageIdOf(localOnly)).isNull()
|
||||
assertThat(storageIdOf(stillRemote)).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given My Story is somehow tombstoned, when its storage id is local only, then I expect it to keep its storage id`() {
|
||||
val deleted = insertList("deleted", deletedAt = System.currentTimeMillis())
|
||||
val myStory = distributionDatabase.getRecipientId(DistributionListId.MY_STORY)!!
|
||||
setDeletionTimestamp(DistributionListId.MY_STORY, System.currentTimeMillis())
|
||||
|
||||
distributionDatabase.removeStorageIdsFromLocalOnlyDeletedLists(listOf(storageIdOf(deleted)!!, storageIdOf(myStory)!!))
|
||||
|
||||
assertThat(storageIdOf(deleted)).isNull()
|
||||
assertThat(storageIdOf(myStory)).isNotNull()
|
||||
}
|
||||
|
||||
private fun createRecipients(count: Int): List<RecipientId> {
|
||||
return (0 until count).map {
|
||||
SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
|
||||
@@ -95,4 +178,26 @@ class DistributionListTablesTest {
|
||||
private fun recipientList(vararg ids: Long): List<RecipientId> {
|
||||
return ids.map { RecipientId.from(it) }
|
||||
}
|
||||
|
||||
private fun insertList(name: String, deletedAt: Long? = null): RecipientId {
|
||||
val listId = distributionDatabase.createList(name, emptyList())!!
|
||||
|
||||
if (deletedAt != null) {
|
||||
distributionDatabase.deleteList(listId, deletedAt)
|
||||
}
|
||||
|
||||
return distributionDatabase.getRecipientId(listId)!!
|
||||
}
|
||||
|
||||
private fun storageIdOf(recipientId: RecipientId): StorageId? {
|
||||
return SignalDatabase.recipients.getContactStorageSyncIdsMap()[recipientId]
|
||||
}
|
||||
|
||||
private fun setDeletionTimestamp(listId: DistributionListId, timestamp: Long) {
|
||||
distributionDatabase.writableDatabase
|
||||
.update(DistributionListTables.ListTable.TABLE_NAME)
|
||||
.values(DistributionListTables.ListTable.DELETION_TIMESTAMP to timestamp)
|
||||
.where("${DistributionListTables.ListTable.ID} = ?", listId.serialize())
|
||||
.run()
|
||||
}
|
||||
}
|
||||
|
||||
+1
-5
@@ -8,7 +8,6 @@ import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.assertions.single
|
||||
import io.mockk.every
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
@@ -29,7 +28,6 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.testutil.RecipientTestRule
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.storage.SignalNotificationProfileRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import java.time.DayOfWeek
|
||||
@@ -50,8 +48,6 @@ class NotificationProfileTablesTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
every { RemoteConfig.messageQueueTime } returns TimeUnit.DAYS.toMillis(45)
|
||||
|
||||
alice = SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
|
||||
|
||||
profile1 = NotificationProfile(
|
||||
@@ -312,7 +308,7 @@ class NotificationProfileTablesTest {
|
||||
)
|
||||
|
||||
SignalDatabase.notificationProfiles.insertNotificationProfileFromStorageSync(remoteRecord)
|
||||
SignalDatabase.notificationProfiles.removeStorageIdsFromOldDeletedProfiles(System.currentTimeMillis())
|
||||
SignalDatabase.notificationProfiles.removeStorageIdsFromOldDeletedProfiles(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(45))
|
||||
assertThat(SignalDatabase.notificationProfiles.getStorageSyncIds()).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import assertk.assertions.isNotEqualTo
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import io.mockk.every
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
@@ -24,7 +23,6 @@ import org.thoughtcrime.securesms.database.model.IncomingSticker
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackId
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.testutil.RecipientTestRule
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.storage.SignalStickerPackRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import java.io.ByteArrayInputStream
|
||||
@@ -48,8 +46,6 @@ class StickerTablesTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
every { RemoteConfig.messageQueueTime } returns TimeUnit.DAYS.toMillis(45)
|
||||
|
||||
SignalDatabase.stickers.writableDatabase.deleteAll(StickerTables.Sticker.TABLE_NAME)
|
||||
SignalDatabase.stickers.writableDatabase.deleteAll(StickerTables.Pack.TABLE_NAME)
|
||||
}
|
||||
@@ -209,7 +205,7 @@ class StickerTablesTest {
|
||||
installPack(packId1, packKey1)
|
||||
SignalDatabase.stickers.uninstallPack(packId1)
|
||||
|
||||
SignalDatabase.stickers.removeStorageIdsFromOldDeletedPacks(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(46))
|
||||
SignalDatabase.stickers.removeStorageIdsFromOldDeletedPacks(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1))
|
||||
|
||||
assertThat(SignalDatabase.stickers.getStorageSyncIds()).isEmpty()
|
||||
assertThat(SignalDatabase.stickers.getPackForStorageSync(StickerPackId(packId1))!!.storageServiceId).isNull()
|
||||
|
||||
@@ -390,6 +390,24 @@ class StorageSyncJobTest {
|
||||
assertEquals(0, remoteStorage.records.count { it.proto.contact != null })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a contact was unregistered recently, when I run, then I keep their storage id`() {
|
||||
val contact = recipients.createRecipient("Local Contact")
|
||||
SignalDatabase.recipients.rotateStorageId(contact)
|
||||
check(runJob(StorageSyncJob.forLocalChange()).isSuccess)
|
||||
check(remoteStorage.records.count { it.proto.contact != null } == 1)
|
||||
|
||||
SignalDatabase.recipients.markUnregistered(contact)
|
||||
Recipient.live(contact).refresh()
|
||||
remoteStorage.resetCounters()
|
||||
|
||||
val result = runJob(StorageSyncJob.forLocalChange())
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertNotNull(storageIdOf(contact))
|
||||
assertEquals(1, remoteStorage.records.count { it.proto.contact != null })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given another device keeps undoing my write, when I run again, then I stop writing`() {
|
||||
stubIssueReporter()
|
||||
|
||||
@@ -1,43 +1,24 @@
|
||||
package org.thoughtcrime.securesms.storage
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.unmockkObject
|
||||
import okio.ByteString
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.signal.core.models.ServiceId.ACI
|
||||
import org.signal.core.models.ServiceId.ACI.Companion.parseOrThrow
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.findIdDifference
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.profileKeyChanged
|
||||
import org.thoughtcrime.securesms.testutil.TestHelpers
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||
import org.whispersystems.signalservice.api.storage.SignalRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
class StorageSyncHelperTest {
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkObject(RemoteConfig)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkObject(RemoteConfig)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun findIdDifference_allOverlap() {
|
||||
every { RemoteConfig.messageQueueTime } returns 45.days.inWholeMilliseconds
|
||||
|
||||
val result = findIdDifference(keyListOf(1, 2, 3), keyListOf(1, 2, 3))
|
||||
assertTrue(result.localOnlyIds.isEmpty())
|
||||
assertTrue(result.remoteOnlyIds.isEmpty())
|
||||
@@ -46,8 +27,6 @@ class StorageSyncHelperTest {
|
||||
|
||||
@Test
|
||||
fun findIdDifference_noOverlap() {
|
||||
every { RemoteConfig.messageQueueTime } returns 45.days.inWholeMilliseconds
|
||||
|
||||
val result = findIdDifference(keyListOf(1, 2, 3), keyListOf(4, 5, 6))
|
||||
TestHelpers.assertContentsEqual(keyListOf(1, 2, 3), result.remoteOnlyIds)
|
||||
TestHelpers.assertContentsEqual(keyListOf(4, 5, 6), result.localOnlyIds)
|
||||
@@ -56,8 +35,6 @@ class StorageSyncHelperTest {
|
||||
|
||||
@Test
|
||||
fun findIdDifference_someOverlap() {
|
||||
every { RemoteConfig.messageQueueTime } returns 45.days.inWholeMilliseconds
|
||||
|
||||
val result = findIdDifference(keyListOf(1, 2, 3), keyListOf(2, 3, 4))
|
||||
TestHelpers.assertContentsEqual(keyListOf(1), result.remoteOnlyIds)
|
||||
TestHelpers.assertContentsEqual(keyListOf(4), result.localOnlyIds)
|
||||
@@ -66,8 +43,6 @@ class StorageSyncHelperTest {
|
||||
|
||||
@Test
|
||||
fun findIdDifference_typeMismatch_allOverlap() {
|
||||
every { RemoteConfig.messageQueueTime } returns 45.days.inWholeMilliseconds
|
||||
|
||||
val result = findIdDifference(
|
||||
keyListOf(
|
||||
mapOf(
|
||||
@@ -90,8 +65,6 @@ class StorageSyncHelperTest {
|
||||
|
||||
@Test
|
||||
fun findIdDifference_typeMismatch_someOverlap() {
|
||||
every { RemoteConfig.messageQueueTime } returns 45.days.inWholeMilliseconds
|
||||
|
||||
val result = findIdDifference(
|
||||
keyListOf(
|
||||
mapOf(
|
||||
@@ -116,8 +89,6 @@ class StorageSyncHelperTest {
|
||||
|
||||
@Test
|
||||
fun test_ContactUpdate_equals_sameProfileKeys() {
|
||||
every { RemoteConfig.messageQueueTime } returns 45.days.inWholeMilliseconds
|
||||
|
||||
val profileKey = ByteArray(32)
|
||||
val profileKeyCopy = profileKey.clone()
|
||||
|
||||
@@ -135,8 +106,6 @@ class StorageSyncHelperTest {
|
||||
|
||||
@Test
|
||||
fun test_ContactUpdate_equals_differentProfileKeys() {
|
||||
every { RemoteConfig.messageQueueTime } returns 45.days.inWholeMilliseconds
|
||||
|
||||
val profileKey = ByteArray(32)
|
||||
val profileKeyCopy = profileKey.clone()
|
||||
profileKeyCopy[0] = 1
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.storage
|
||||
|
||||
import android.app.Application
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.isPresent
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.signal.core.util.UuidUtil
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.testutil.RecipientTestRule
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StoryDistributionListRecord
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class StoryDistributionListRecordProcessorAgeOffTest {
|
||||
|
||||
@get:Rule
|
||||
val recipients = RecipientTestRule()
|
||||
|
||||
private val testSubject = StoryDistributionListRecordProcessor()
|
||||
|
||||
@Test
|
||||
fun `given an aged off list tombstone, when the remote manifest still has its id, then I expect a matching record with a fresh storage id`() {
|
||||
val deletedAt = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(46)
|
||||
val distributionId = DistributionId.from(UUID.randomUUID())
|
||||
val listId = insertAgedOffList(distributionId, deletedAt)
|
||||
|
||||
val matching = testSubject.getMatching(remoteTombstone(distributionId, deletedAt)) { StorageSyncHelper.generateKey() }
|
||||
|
||||
assertThat(matching).isPresent()
|
||||
assertThat(storageIdOf(listId)).isNotNull()
|
||||
}
|
||||
|
||||
private fun insertAgedOffList(distributionId: DistributionId, deletedAt: Long): DistributionListId {
|
||||
val listId = SignalDatabase.distributionLists.createList("test", emptyList(), distributionId = distributionId)!!
|
||||
SignalDatabase.distributionLists.deleteList(listId, deletedAt)
|
||||
SignalDatabase.distributionLists.removeStorageIdsFromOldDeletedLists(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(45))
|
||||
|
||||
return listId
|
||||
}
|
||||
|
||||
private fun remoteTombstone(distributionId: DistributionId, deletedAt: Long): SignalStoryDistributionListRecord {
|
||||
return SignalStoryDistributionListRecord(
|
||||
StorageId.forStoryDistributionList(byteArrayOf(1, 2, 3, 4)),
|
||||
StoryDistributionListRecord()
|
||||
.newBuilder()
|
||||
.identifier(UuidUtil.toByteArray(distributionId.asUuid()).toByteString())
|
||||
.deletedAtTimestamp(deletedAt)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun storageIdOf(listId: DistributionListId): StorageId? {
|
||||
val recipientId = SignalDatabase.distributionLists.getRecipientId(listId)!!
|
||||
return SignalDatabase.recipients.getContactStorageSyncIdsMap()[recipientId]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user