Ensure story media is only uploaded once.

This commit is contained in:
Alex Hart
2022-06-23 17:20:23 -03:00
committed by Cody Henthorne
parent 6b745ba58a
commit ebc556801e
6 changed files with 617 additions and 12 deletions

View File

@@ -0,0 +1,240 @@
package org.thoughtcrime.securesms.sms
import android.app.Application
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JobManager
import org.thoughtcrime.securesms.jobs.AttachmentCompressionJob
import org.thoughtcrime.securesms.jobs.AttachmentCopyJob
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob
import org.thoughtcrime.securesms.jobs.ResumableUploadSpecJob
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.testutil.OutgoingMediaMessageBuilder
import org.thoughtcrime.securesms.testutil.OutgoingMediaMessageBuilder.secure
import org.thoughtcrime.securesms.testutil.UriAttachmentBuilder
import org.thoughtcrime.securesms.util.JsonUtils
import org.thoughtcrime.securesms.util.MediaUtil
import java.util.concurrent.atomic.AtomicLong
/**
* Requires Robolectric due to usage of Uri
*/
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class)
class UploadDependencyGraphTest {
private val jobManager: JobManager = mock()
private var uniqueLong = AtomicLong(0)
@Before
fun setUp() {
whenever(jobManager.startChain(any<Job>())).then {
JobManager.Chain(jobManager, listOf(it.getArgument(0)))
}
}
@Test
fun `Given a list of Uri attachments and a list of Messages, when I get the dependencyMap, then I expect a times m results`() {
// GIVEN
val uriAttachments = (1..5).map { UriAttachmentBuilder.build(id = uniqueLong.getAndIncrement(), contentType = MediaUtil.IMAGE_JPEG) }
val messages = (1..5).map { OutgoingMediaMessageBuilder.create(attachments = uriAttachments).secure() }
val testSubject = UploadDependencyGraph.create(messages, jobManager) { getAttachmentForPreUpload(uniqueLong.getAndIncrement(), it) }
// WHEN
val result = testSubject.dependencyMap
// THEN
assertEquals(5, result.size)
result.values.forEach { assertEquals(5, it.size) }
}
@Test
fun `Given a list of Uri attachments and a list of Messages, when I consumeDeferredQueue, then I expect one upload chain and one copy job for each attachment`() {
// GIVEN
val uriAttachments = (1..5).map { UriAttachmentBuilder.build(id = uniqueLong.getAndIncrement(), contentType = MediaUtil.IMAGE_JPEG) }
val messages = (1..5).map { OutgoingMediaMessageBuilder.create(attachments = uriAttachments).secure() }
val testSubject = UploadDependencyGraph.create(messages, jobManager) { getAttachmentForPreUpload(uniqueLong.getAndIncrement(), it) }
// WHEN
val deferredQueue = testSubject.consumeDeferredQueue()
// THEN
deferredQueue.forEach { assertValidJobChain(it, 4) }
}
@Test
fun `Given a list of Uri attachments with same id but different transforms and a list of Messages, when I consumeDeferredQueue, then I expect one upload chain for each attachment and 5 copy jobs`() {
// GIVEN
val uriAttachments = (1..5).map {
val increment = uniqueLong.getAndIncrement()
UriAttachmentBuilder.build(
id = 10,
contentType = MediaUtil.IMAGE_JPEG,
transformProperties = AttachmentDatabase.TransformProperties(false, true, increment, increment + 1, SentMediaQuality.STANDARD.code)
)
}
val messages = (1..5).map { OutgoingMediaMessageBuilder.create(attachments = uriAttachments).secure() }
val testSubject = UploadDependencyGraph.create(messages, jobManager) { getAttachmentForPreUpload(uniqueLong.getAndIncrement(), it) }
// WHEN
val deferredQueue = testSubject.consumeDeferredQueue()
// THEN
deferredQueue.forEach { assertValidJobChain(it, 4) }
}
@Test
fun `Given a single Uri attachment with same id and a list of Messages, when I consumeDeferredQueue, then I expect one upload chain and one copy job`() {
// GIVEN
val uriAttachments = (1..5).map {
UriAttachmentBuilder.build(
id = 10,
contentType = MediaUtil.IMAGE_JPEG
)
}
val messages = (1..5).map { OutgoingMediaMessageBuilder.create(attachments = uriAttachments).secure() }
val testSubject = UploadDependencyGraph.create(messages, jobManager) { getAttachmentForPreUpload(uniqueLong.getAndIncrement(), it) }
// WHEN
val deferredQueue = testSubject.consumeDeferredQueue()
// THEN
assertEquals(1, deferredQueue.size)
deferredQueue.forEach { assertValidJobChain(it, 4) }
}
@Test
fun `Given three Uri attachments with same id and two share transform properties and a list of Messages, when I executeDeferredQueue, then I expect two chains`() {
// GIVEN
val uriAttachments = (1..3).map {
UriAttachmentBuilder.build(
id = 10,
contentType = MediaUtil.IMAGE_JPEG,
transformProperties = if (it != 1) AttachmentDatabase.TransformProperties(false, true, 1, 2, SentMediaQuality.STANDARD.code) else null
)
}
val messages = (1..8).map { OutgoingMediaMessageBuilder.create(attachments = uriAttachments).secure() }
val testSubject = UploadDependencyGraph.create(messages, jobManager) { getAttachmentForPreUpload(uniqueLong.getAndIncrement(), it) }
// WHEN
val deferredQueue = testSubject.consumeDeferredQueue()
// THEN
assertEquals(2, deferredQueue.size)
deferredQueue.forEach { assertValidJobChain(it, 7) }
}
@Test
fun `Given a list of Database attachments and a list of Messages, when I get the dependency map, then I expect a times m results`() {
// GIVEN
val databaseAttachments = (1..5).map {
val id = uniqueLong.getAndIncrement()
val uriAttachment = UriAttachmentBuilder.build(id = uniqueLong.getAndIncrement(), contentType = MediaUtil.IMAGE_JPEG)
getAttachmentForPreUpload(id, uriAttachment)
}
val messages = (1..5).map { OutgoingMediaMessageBuilder.create(attachments = databaseAttachments).secure() }
val testSubject = UploadDependencyGraph.create(messages, jobManager) { getAttachmentForPreUpload(uniqueLong.getAndIncrement(), it) }
// WHEN
val result = testSubject.dependencyMap
// THEN
assertEquals(5, result.size)
result.values.forEach { assertEquals(5, it.size) }
}
@Test
fun `Given a list of messages with unique ids, when I consumeDeferredQueue, then I expect no copy jobs`() {
// GIVEN
val attachment1 = UriAttachmentBuilder.build(uniqueLong.getAndIncrement(), contentType = MediaUtil.IMAGE_JPEG)
val attachment2 = UriAttachmentBuilder.build(uniqueLong.getAndIncrement(), contentType = MediaUtil.IMAGE_JPEG)
val message1 = OutgoingMediaMessageBuilder.create(attachments = listOf(attachment1))
val message2 = OutgoingMediaMessageBuilder.create(attachments = listOf(attachment2))
val testSubject = UploadDependencyGraph.create(listOf(message1, message2), jobManager) { getAttachmentForPreUpload(uniqueLong.getAndIncrement(), it) }
// WHEN
val result = testSubject.consumeDeferredQueue()
// THEN
assertEquals(2, result.size)
result.forEach {
assertValidJobChain(it, 0)
}
}
private fun assertValidJobChain(chain: JobManager.Chain, expectedCopyDestinationCount: Int) {
val steps: List<List<Job>> = chain.jobListChain
assertTrue(steps.all { it.size == 1 })
assertTrue(steps[0][0] is AttachmentCompressionJob)
assertTrue(steps[1][0] is ResumableUploadSpecJob)
assertTrue(steps[2][0] is AttachmentUploadJob)
if (expectedCopyDestinationCount > 0) {
assertTrue(steps[3][0] is AttachmentCopyJob)
val uploadData = steps[2][0].serialize()
val copyData = steps[3][0].serialize()
val uploadAttachmentId = AttachmentId(uploadData.getLong("row_id"), uploadData.getLong("unique_id"))
val copySourceAttachmentId = JsonUtils.fromJson(copyData.getString("source_id"), AttachmentId::class.java)
assertEquals(uploadAttachmentId, copySourceAttachmentId)
val copyDestinations = copyData.getStringArray("destination_ids")
assertEquals(expectedCopyDestinationCount, copyDestinations.size)
} else {
assertEquals(3, steps.size)
}
}
private fun getAttachmentForPreUpload(id: Long, attachment: Attachment): DatabaseAttachment {
return DatabaseAttachment(
AttachmentId(id, id),
AttachmentDatabase.PREUPLOAD_MESSAGE_ID,
false,
false,
attachment.contentType,
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
attachment.size,
attachment.fileName,
attachment.cdnNumber,
attachment.location,
attachment.key,
attachment.relay,
attachment.digest,
attachment.fastPreflightId,
attachment.isVoiceNote,
attachment.isBorderless,
attachment.isVideoGif,
attachment.width,
attachment.height,
attachment.isQuote,
attachment.caption,
attachment.sticker,
attachment.blurHash,
attachment.audioHash,
attachment.transformProperties,
0,
attachment.uploadTimestamp
)
}
}

View File

@@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.testutil
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch
import org.thoughtcrime.securesms.database.documents.NetworkFailure
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.ParentStoryId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.recipients.Recipient
object OutgoingMediaMessageBuilder {
fun create(
recipient: Recipient = Recipient.UNKNOWN,
message: String = "",
attachments: List<Attachment> = emptyList(),
sentTimeMillis: Long = System.currentTimeMillis(),
subscriptionId: Int = -1,
expiresIn: Long = -1,
viewOnce: Boolean = false,
distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
storyType: StoryType = StoryType.NONE,
parentStoryId: ParentStoryId? = null,
isStoryReaction: Boolean = false,
quoteModel: QuoteModel? = null,
contacts: List<Contact> = emptyList(),
linkPreviews: List<LinkPreview> = emptyList(),
mentions: List<Mention> = emptyList(),
networkFailures: Set<NetworkFailure> = emptySet(),
identityKeyMismatches: Set<IdentityKeyMismatch> = emptySet(),
giftBadge: GiftBadge? = null
): OutgoingMediaMessage {
return OutgoingMediaMessage(
recipient,
message,
attachments,
sentTimeMillis,
subscriptionId,
expiresIn,
viewOnce,
distributionType,
storyType,
parentStoryId,
isStoryReaction,
quoteModel,
contacts,
linkPreviews,
mentions,
networkFailures,
identityKeyMismatches,
giftBadge
)
}
fun OutgoingMediaMessage.secure(): OutgoingSecureMediaMessage = OutgoingSecureMediaMessage(this)
}

View File

@@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.testutil
import android.net.Uri
import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.audio.AudioHash
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.stickers.StickerLocator
object UriAttachmentBuilder {
fun build(
id: Long,
uri: Uri = Uri.parse("content://$id"),
contentType: String,
transferState: Int = AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
size: Long = 0L,
fileName: String = "file$id",
voiceNote: Boolean = false,
borderless: Boolean = false,
videoGif: Boolean = false,
quote: Boolean = false,
caption: String? = null,
stickerLocator: StickerLocator? = null,
blurHash: BlurHash? = null,
audioHash: AudioHash? = null,
transformProperties: AttachmentDatabase.TransformProperties? = null
): UriAttachment {
return UriAttachment(
uri,
contentType,
transferState,
size,
fileName,
voiceNote,
borderless,
videoGif,
quote,
caption,
stickerLocator,
blurHash,
audioHash,
transformProperties
)
}
}