Skip to content

Commit a81a675

Browse files
committed
Add Delete for Me sync support.
1 parent 1c66da7 commit a81a675

40 files changed

Lines changed: 2274 additions & 198 deletions

app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.thoughtcrime.securesms.dependencies
22

33
import android.app.Application
4+
import io.mockk.spyk
45
import okhttp3.ConnectionSpec
56
import okhttp3.Response
67
import okhttp3.WebSocket
@@ -23,6 +24,9 @@ import org.thoughtcrime.securesms.testing.Get
2324
import org.thoughtcrime.securesms.testing.Verb
2425
import org.thoughtcrime.securesms.testing.runSync
2526
import org.thoughtcrime.securesms.testing.success
27+
import org.whispersystems.signalservice.api.SignalServiceDataStore
28+
import org.whispersystems.signalservice.api.SignalServiceMessageSender
29+
import org.whispersystems.signalservice.api.SignalWebSocket
2630
import org.whispersystems.signalservice.api.push.TrustStore
2731
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
2832
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
@@ -43,6 +47,7 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
4347
private val uncensoredConfiguration: SignalServiceConfiguration
4448
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
4549
private val recipientCache: LiveRecipientCache
50+
private var signalServiceMessageSender: SignalServiceMessageSender? = null
4651

4752
init {
4853
runSync {
@@ -101,6 +106,17 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
101106
return recipientCache
102107
}
103108

109+
override fun provideSignalServiceMessageSender(
110+
signalWebSocket: SignalWebSocket,
111+
protocolStore: SignalServiceDataStore,
112+
signalServiceConfiguration: SignalServiceConfiguration
113+
): SignalServiceMessageSender {
114+
if (signalServiceMessageSender == null) {
115+
signalServiceMessageSender = spyk(objToCopy = default.provideSignalServiceMessageSender(signalWebSocket, protocolStore, signalServiceConfiguration))
116+
}
117+
return signalServiceMessageSender!!
118+
}
119+
104120
class MockWebSocket : WebSocketListener() {
105121
private val TAG = "MockWebSocket"
106122

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* Copyright 2024 Signal Messenger, LLC
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
package org.thoughtcrime.securesms.jobs
7+
8+
import androidx.test.ext.junit.runners.AndroidJUnit4
9+
import io.mockk.CapturingSlot
10+
import io.mockk.every
11+
import io.mockk.mockkStatic
12+
import io.mockk.slot
13+
import io.mockk.unmockkStatic
14+
import okio.ByteString.Companion.toByteString
15+
import org.junit.After
16+
import org.junit.Before
17+
import org.junit.Rule
18+
import org.junit.Test
19+
import org.junit.runner.RunWith
20+
import org.thoughtcrime.securesms.database.SignalDatabase
21+
import org.thoughtcrime.securesms.database.model.MessageRecord
22+
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
23+
import org.thoughtcrime.securesms.jobs.protos.DeleteSyncJobData
24+
import org.thoughtcrime.securesms.messages.MessageHelper
25+
import org.thoughtcrime.securesms.recipients.Recipient
26+
import org.thoughtcrime.securesms.recipients.RecipientId
27+
import org.thoughtcrime.securesms.testing.SignalActivityRule
28+
import org.thoughtcrime.securesms.testing.assertIs
29+
import org.thoughtcrime.securesms.testing.assertIsNotNull
30+
import org.thoughtcrime.securesms.testing.assertIsSize
31+
import org.thoughtcrime.securesms.util.MessageTableTestUtils
32+
import org.thoughtcrime.securesms.util.TextSecurePreferences
33+
import org.whispersystems.signalservice.api.messages.SendMessageResult
34+
import org.whispersystems.signalservice.api.push.SignalServiceAddress
35+
import org.whispersystems.signalservice.internal.push.Content
36+
import java.util.Optional
37+
38+
@RunWith(AndroidJUnit4::class)
39+
class MultiDeviceDeleteSendSyncJobTest {
40+
41+
@get:Rule
42+
val harness = SignalActivityRule(createGroup = true)
43+
44+
private lateinit var messageHelper: MessageHelper
45+
46+
private lateinit var success: SendMessageResult
47+
private lateinit var failure: SendMessageResult
48+
private lateinit var content: CapturingSlot<Content>
49+
50+
@Before
51+
fun setUp() {
52+
messageHelper = MessageHelper(harness)
53+
54+
mockkStatic(TextSecurePreferences::class)
55+
every { TextSecurePreferences.isMultiDevice(any()) } answers {
56+
true
57+
}
58+
59+
success = SendMessageResult.success(SignalServiceAddress(Recipient.self().requireServiceId()), listOf(2), true, false, 0, Optional.empty())
60+
failure = SendMessageResult.networkFailure(SignalServiceAddress(Recipient.self().requireServiceId()))
61+
content = slot<Content>()
62+
}
63+
64+
@After
65+
fun tearDown() {
66+
messageHelper.tearDown()
67+
68+
unmockkStatic(TextSecurePreferences::class)
69+
}
70+
71+
@Test
72+
fun messageDeletes() {
73+
// GIVEN
74+
val messages = mutableListOf<MessageHelper.MessageData>()
75+
messages += messageHelper.incomingText()
76+
messages += messageHelper.incomingText()
77+
messages += messageHelper.outgoingText()
78+
79+
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
80+
val records: Set<MessageRecord> = MessageTableTestUtils.getMessages(threadId).toSet()
81+
82+
// WHEN
83+
every { ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(capture(content), any(), any()) } returns success
84+
85+
val job = MultiDeviceDeleteSendSyncJob.createMessageDeletes(records)
86+
val result = job.run()
87+
88+
// THEN
89+
result.isSuccess assertIs true
90+
assertDeleteSync(messageHelper.alice, messages)
91+
}
92+
93+
@Test
94+
fun groupMessageDeletes() {
95+
// GIVEN
96+
val messages = mutableListOf<MessageHelper.MessageData>()
97+
messages += messageHelper.incomingText(destination = messageHelper.group.recipientId)
98+
messages += messageHelper.incomingText(destination = messageHelper.group.recipientId)
99+
messages += messageHelper.outgoingText(conversationId = messageHelper.group.recipientId)
100+
101+
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
102+
val records: Set<MessageRecord> = MessageTableTestUtils.getMessages(threadId).toSet()
103+
104+
// WHEN
105+
every { ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(capture(content), any(), any()) } returns success
106+
107+
val job = MultiDeviceDeleteSendSyncJob.createMessageDeletes(records)
108+
val result = job.run()
109+
110+
// THEN
111+
result.isSuccess assertIs true
112+
assertDeleteSync(messageHelper.group.recipientId, messages)
113+
}
114+
115+
@Test
116+
fun retryOfDeletes() {
117+
// GIVEN
118+
val alice = messageHelper.alice.toLong()
119+
120+
// WHEN
121+
every { ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(capture(content), any(), any()) } returns failure
122+
123+
val job = MultiDeviceDeleteSendSyncJob(
124+
messages = listOf(DeleteSyncJobData.AddressableMessage(alice, 1, alice)),
125+
threads = listOf(DeleteSyncJobData.ThreadDelete(alice, listOf(DeleteSyncJobData.AddressableMessage(alice, 1, alice)))),
126+
localOnlyThreads = listOf(DeleteSyncJobData.ThreadDelete(alice))
127+
)
128+
129+
val result = job.run()
130+
val data = DeleteSyncJobData.ADAPTER.decode(job.serialize())
131+
132+
// THEN
133+
result.isRetry assertIs true
134+
data.messageDeletes.assertIsSize(1)
135+
data.threadDeletes.assertIsSize(1)
136+
data.localOnlyThreadDeletes.assertIsSize(1)
137+
}
138+
139+
private fun assertDeleteSync(conversation: RecipientId, inputMessages: List<MessageHelper.MessageData>) {
140+
val messagesMap = inputMessages.associateBy { it.timestamp }
141+
142+
val content = this.content.captured
143+
144+
content.syncMessage?.padding.assertIsNotNull()
145+
content.syncMessage?.deleteForMe.assertIsNotNull()
146+
147+
val deleteForMe = content.syncMessage!!.deleteForMe!!
148+
deleteForMe.messageDeletes.assertIsSize(1)
149+
deleteForMe.conversationDeletes.assertIsSize(0)
150+
deleteForMe.localOnlyConversationDeletes.assertIsSize(0)
151+
152+
val messageDeletes = deleteForMe.messageDeletes[0]
153+
val conversationRecipient = Recipient.resolved(conversation)
154+
if (conversationRecipient.isGroup) {
155+
messageDeletes.conversation!!.threadGroupId assertIs conversationRecipient.requireGroupId().decodedId.toByteString()
156+
} else {
157+
messageDeletes.conversation!!.threadAci assertIs conversationRecipient.requireAci().toString()
158+
}
159+
160+
messageDeletes
161+
.messages
162+
.forEach { delete ->
163+
val messageData = messagesMap[delete.sentTimestamp]
164+
delete.sentTimestamp assertIs messageData!!.timestamp
165+
delete.authorAci assertIs Recipient.resolved(messageData.author).requireAci().toString()
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)