diff --git a/sms-exporter/lib/build.gradle b/sms-exporter/lib/build.gradle index cea793646..e9260ecf9 100644 --- a/sms-exporter/lib/build.gradle +++ b/sms-exporter/lib/build.gradle @@ -15,4 +15,12 @@ dependencies { implementation libs.rxjava3.rxjava implementation libs.rxjava3.rxandroid implementation libs.rxjava3.rxkotlin + + testImplementation testLibs.junit.junit + testImplementation testLibs.mockito.core + testImplementation testLibs.mockito.android + testImplementation testLibs.mockito.kotlin + testImplementation testLibs.robolectric.robolectric + testImplementation testLibs.androidx.test.core + testImplementation testLibs.androidx.test.core.ktx } \ No newline at end of file diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCase.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCase.kt index 007011ca8..bf4a5d5ae 100644 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCase.kt +++ b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCase.kt @@ -17,6 +17,10 @@ internal object ExportMmsMessagesUseCase { private val TAG = Log.tag(ExportMmsMessagesUseCase::class.java) + internal fun getTransactionId(mms: ExportableMessage.Mms): String { + return "signal:T${mms.id}" + } + fun execute( context: Context, getOrCreateThreadOutput: GetOrCreateMmsThreadIdsUseCase.Output, @@ -24,7 +28,7 @@ internal object ExportMmsMessagesUseCase { ): Try { try { val (mms, threadId) = getOrCreateThreadOutput - val transactionId = "signal:T${mms.id}" + val transactionId = getTransactionId(mms) if (checkForExistence) { Log.d(TAG, "Checking if the message is already in the database.") @@ -47,7 +51,8 @@ internal object ExportMmsMessagesUseCase { Telephony.Mms.MESSAGE_CLASS to "personal", Telephony.Mms.PRIORITY to PduHeaders.PRIORITY_NORMAL, Telephony.Mms.TRANSACTION_ID to transactionId, - Telephony.Mms.RESPONSE_STATUS to PduHeaders.RESPONSE_STATUS_OK + Telephony.Mms.RESPONSE_STATUS to PduHeaders.RESPONSE_STATUS_OK, + Telephony.Mms.SEEN to 1 ) val uri = context.contentResolver.insert(Telephony.Mms.CONTENT_URI, mmsContentValues) diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCase.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCase.kt index 4c61a420e..999cfc4ab 100644 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCase.kt +++ b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCase.kt @@ -17,10 +17,14 @@ internal object ExportMmsPartsUseCase { private val TAG = Log.tag(ExportMmsPartsUseCase::class.java) + internal fun getContentId(part: ExportableMessage.Mms.Part): String { + return "" + } + fun execute(context: Context, part: ExportableMessage.Mms.Part, output: ExportMmsMessagesUseCase.Output, checkForExistence: Boolean): Try { try { val (message, messageId) = output - val contentId = "" + val contentId = getContentId(part) val mmsPartUri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(messageId.toString()).appendPath("part").build() if (checkForExistence) { diff --git a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCase.kt b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCase.kt index a27c2123f..56d7f22ef 100644 --- a/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCase.kt +++ b/sms-exporter/lib/src/main/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCase.kt @@ -43,7 +43,7 @@ internal object GetOrCreateMmsThreadIdsUseCase { error("Expected non-empty recipient count.") } - return HashSet(recipients.map { it.toString() }) + return HashSet(recipients.map { it }) } data class Output(val mms: ExportableMessage.Mms, val threadId: Long) diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/InMemoryContentProvider.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/InMemoryContentProvider.kt new file mode 100644 index 000000000..4bf9a0ce4 --- /dev/null +++ b/sms-exporter/lib/src/test/java/org/signal/smsexporter/InMemoryContentProvider.kt @@ -0,0 +1,114 @@ +package org.signal.smsexporter + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.net.Uri +import android.provider.Telephony +import androidx.test.core.app.ApplicationProvider + +/** + * Provides a content provider which reads and writes to an in-memory database. + */ +class InMemoryContentProvider : ContentProvider() { + + private val database: InMemoryDatabase = InMemoryDatabase() + + override fun onCreate(): Boolean { + return false + } + + override fun query(p0: Uri, p1: Array?, p2: String?, p3: Array?, p4: String?): Cursor? { + val tableName = if (p0.pathSegments.isNotEmpty()) p0.lastPathSegment else p0.authority + return database.readableDatabase.query(tableName, p1, p2, p3, p4, null, null) + } + + override fun getType(p0: Uri): String? { + return null + } + + override fun insert(p0: Uri, p1: ContentValues?): Uri? { + val tableName = if (p0.pathSegments.isNotEmpty()) p0.lastPathSegment else p0.authority + val id = database.writableDatabase.insert(tableName, null, p1) + return if (id == -1L) { + null + } else { + p0.buildUpon().appendPath("$id").build() + } + } + + override fun delete(p0: Uri, p1: String?, p2: Array?): Int { + return -1 + } + + override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array?): Int { + return -1 + } + + private class InMemoryDatabase : SQLiteOpenHelper(ApplicationProvider.getApplicationContext(), null, null, 1) { + override fun onCreate(db: SQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE sms ( + ${Telephony.Sms._ID} INTEGER PRIMARY KEY AUTOINCREMENT, + ${Telephony.Sms.ADDRESS} TEXT, + ${Telephony.Sms.DATE_SENT} INTEGER, + ${Telephony.Sms.DATE} INTEGER, + ${Telephony.Sms.BODY} TEXT, + ${Telephony.Sms.READ} INTEGER, + ${Telephony.Sms.TYPE} INTEGER + ); + + """.trimIndent() + ) + + db.execSQL( + """ + CREATE TABLE mms ( + ${Telephony.Mms._ID} INTEGER PRIMARY KEY AUTOINCREMENT, + ${Telephony.Mms.THREAD_ID} INTEGER, + ${Telephony.Mms.DATE} INTEGER, + ${Telephony.Mms.DATE_SENT} INTEGER, + ${Telephony.Mms.MESSAGE_BOX} INTEGER, + ${Telephony.Mms.READ} INTEGER, + ${Telephony.Mms.CONTENT_TYPE} TEXT, + ${Telephony.Mms.MESSAGE_TYPE} INTEGER, + ${Telephony.Mms.MMS_VERSION} INTEGER, + ${Telephony.Mms.MESSAGE_CLASS} TEXT, + ${Telephony.Mms.PRIORITY} INTEGER, + ${Telephony.Mms.TRANSACTION_ID} TEXT, + ${Telephony.Mms.RESPONSE_STATUS} INTEGER, + ${Telephony.Mms.SEEN} INTEGER + ); + """.trimIndent() + ) + + db.execSQL( + """ + CREATE TABLE part ( + ${Telephony.Mms.Part._ID} INTEGER PRIMARY KEY AUTOINCREMENT, + ${Telephony.Mms.Part.MSG_ID} INTEGER, + ${Telephony.Mms.Part.CONTENT_TYPE} TEXT, + ${Telephony.Mms.Part.CONTENT_ID} INTEGER, + ${Telephony.Mms.Part.TEXT} TEXT + ) + """.trimIndent() + ) + + db.execSQL( + """ + CREATE TABLE addr ( + ${Telephony.Mms.Addr._ID} INTEGER PRIMARY KEY AUTOINCREMENT, + ${Telephony.Mms.Addr.ADDRESS} TEXT, + ${Telephony.Mms.Addr.CHARSET} INTEGER, + ${Telephony.Mms.Addr.TYPE} INTEGER + ) + """.trimIndent() + ) + } + + override fun onUpgrade(db: SQLiteDatabase, p1: Int, p2: Int) = Unit + } +} diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/TestUtils.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/TestUtils.kt new file mode 100644 index 000000000..1b607c9e5 --- /dev/null +++ b/sms-exporter/lib/src/test/java/org/signal/smsexporter/TestUtils.kt @@ -0,0 +1,46 @@ +package org.signal.smsexporter + +import android.provider.Telephony +import org.robolectric.shadows.ShadowContentResolver +import java.util.UUID + +object TestUtils { + fun generateSmsMessage( + id: String = UUID.randomUUID().toString(), + address: String = "+15555060177", + dateReceived: Long = 2, + dateSent: Long = 1, + isRead: Boolean = false, + isOutgoing: Boolean = false, + body: String = "Hello, $id" + ): ExportableMessage.Sms { + return ExportableMessage.Sms(id, address, dateReceived, dateSent, isRead, isOutgoing, body) + } + + fun generateMmsMessage( + id: String = UUID.randomUUID().toString(), + addresses: Set = setOf("+15555060177"), + dateReceived: Long = 2, + dateSent: Long = 1, + isRead: Boolean = false, + isOutgoing: Boolean = false, + parts: List = listOf(ExportableMessage.Mms.Part.Text("Hello, $id")), + sender: CharSequence = "+15555060177" + ): ExportableMessage.Mms { + return ExportableMessage.Mms(id, addresses, dateReceived, dateSent, isRead, isOutgoing, parts, sender) + } + + fun setUpSmsContentProviderAndResolver() { + ShadowContentResolver.registerProviderInternal( + Telephony.Sms.CONTENT_URI.authority, + InMemoryContentProvider() + ) + } + + fun setUpMmsContentProviderAndResolver() { + ShadowContentResolver.registerProviderInternal( + Telephony.Mms.CONTENT_URI.authority, + InMemoryContentProvider() + ) + } +} diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCaseTest.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCaseTest.kt new file mode 100644 index 000000000..8c86c7771 --- /dev/null +++ b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsMessagesUseCaseTest.kt @@ -0,0 +1,133 @@ +package org.signal.smsexporter.internal.mms + +import android.content.Context +import android.net.Uri +import android.provider.Telephony +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.signal.core.util.CursorUtil +import org.signal.smsexporter.ExportableMessage +import org.signal.smsexporter.TestUtils + +@RunWith(RobolectricTestRunner::class) +class ExportMmsMessagesUseCaseTest { + + @Before + fun setUp() { + TestUtils.setUpMmsContentProviderAndResolver() + } + + @Test + fun `Given an MMS message, when I execute, then I expect an MMS record to be created`() { + // GIVEN + val mmsMessage = TestUtils.generateMmsMessage() + val threadUseCaseOutput = GetOrCreateMmsThreadIdsUseCase.Output(mmsMessage, 1) + + // WHEN + val result = ExportMmsMessagesUseCase.execute( + ApplicationProvider.getApplicationContext(), + threadUseCaseOutput, + false + ) + + // THEN + result.either( + onSuccess = { + validateExportedMessage(mmsMessage) + }, + onFailure = { + throw it + } + ) + } + + @Test + fun `Given an MMS message that already exists, when I execute and check for existence, then I expect no new MMS record to be created`() { + // GIVEN + val mmsMessage = TestUtils.generateMmsMessage() + val threadUseCaseOutput = GetOrCreateMmsThreadIdsUseCase.Output(mmsMessage, 1) + ExportMmsMessagesUseCase.execute( + ApplicationProvider.getApplicationContext(), + threadUseCaseOutput, + false + ) + + // WHEN + val result = ExportMmsMessagesUseCase.execute( + ApplicationProvider.getApplicationContext(), + threadUseCaseOutput, + true + ) + + // THEN + result.either( + onSuccess = { + validateExportedMessage(mmsMessage) + }, + onFailure = { + throw it + } + ) + } + + @Test + fun `Given an MMS message that already exists, when I execute and do not check for existence, then I expect a duplicate MMS record to be created`() { + // GIVEN + val mmsMessage = TestUtils.generateMmsMessage() + val threadUseCaseOutput = GetOrCreateMmsThreadIdsUseCase.Output(mmsMessage, 1) + ExportMmsMessagesUseCase.execute( + ApplicationProvider.getApplicationContext(), + threadUseCaseOutput, + false + ) + + // WHEN + val result = ExportMmsMessagesUseCase.execute( + ApplicationProvider.getApplicationContext(), + threadUseCaseOutput, + false + ) + + // THEN + result.either( + onSuccess = { + validateExportedMessage(mmsMessage, expectedRowCount = 2) + }, + onFailure = { + throw it + } + ) + } + + private fun validateExportedMessage( + mms: ExportableMessage.Mms, + expectedRowCount: Int = 1, + threadId: Long = 1L + ) { + val context: Context = ApplicationProvider.getApplicationContext() + val baseUri: Uri = Telephony.Mms.CONTENT_URI + val transactionId = ExportMmsMessagesUseCase.getTransactionId(mms) + + context.contentResolver.query( + baseUri, + null, + "${Telephony.Mms.TRANSACTION_ID} = ?", + arrayOf(transactionId), + null, + null + )?.use { + it.moveToFirst() + assertEquals(expectedRowCount, it.count) + assertEquals(threadId, CursorUtil.requireLong(it, Telephony.Mms.THREAD_ID)) + assertEquals(mms.dateReceived, CursorUtil.requireLong(it, Telephony.Mms.DATE)) + assertEquals(mms.dateSent, CursorUtil.requireLong(it, Telephony.Mms.DATE_SENT)) + assertEquals(if (mms.isOutgoing) Telephony.Mms.MESSAGE_BOX_SENT else Telephony.Mms.MESSAGE_BOX_INBOX, CursorUtil.requireInt(it, Telephony.Mms.MESSAGE_BOX)) + assertEquals(mms.isRead, CursorUtil.requireBoolean(it, Telephony.Mms.READ)) + assertEquals(transactionId, CursorUtil.requireString(it, Telephony.Mms.TRANSACTION_ID)) + } ?: org.junit.Assert.fail("Content Resolver returned a null cursor") + } +} diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCaseTest.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCaseTest.kt new file mode 100644 index 000000000..b6b30833b --- /dev/null +++ b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsPartsUseCaseTest.kt @@ -0,0 +1,136 @@ +package org.signal.smsexporter.internal.mms + +import android.content.Context +import android.net.Uri +import android.provider.Telephony +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.signal.core.util.CursorUtil +import org.signal.smsexporter.ExportableMessage +import org.signal.smsexporter.TestUtils + +@RunWith(RobolectricTestRunner::class) +class ExportMmsPartsUseCaseTest { + + @Before + fun setUp() { + TestUtils.setUpMmsContentProviderAndResolver() + } + + @Test + fun `Given a message with a part, when I export part, then I expect a valid part row`() { + // GIVEN + val message = TestUtils.generateMmsMessage() + val output = ExportMmsMessagesUseCase.Output(message, 1) + + // WHEN + val result = ExportMmsPartsUseCase.execute( + ApplicationProvider.getApplicationContext(), + message.parts.first(), + output, + false + ) + + // THEN + result.either( + onSuccess = { + validateExportedPart(message.parts.first(), output.messageId) + }, + onFailure = { + throw it + } + ) + } + + @Test + fun `Given an already exported part, when I export part with check, then I expect a single part row`() { + // GIVEN + val message = TestUtils.generateMmsMessage() + val output = ExportMmsMessagesUseCase.Output(message, 1) + ExportMmsPartsUseCase.execute( + ApplicationProvider.getApplicationContext(), + message.parts.first(), + output, + false + ) + + // WHEN + val result = ExportMmsPartsUseCase.execute( + ApplicationProvider.getApplicationContext(), + message.parts.first(), + output, + true + ) + + // THEN + result.either( + onSuccess = { + validateExportedPart(message.parts.first(), output.messageId) + }, + onFailure = { + throw it + } + ) + } + + @Test + fun `Given an already exported part, when I export part without check, then I expect a duplicated part row`() { + // GIVEN + val message = TestUtils.generateMmsMessage() + val output = ExportMmsMessagesUseCase.Output(message, 1) + ExportMmsPartsUseCase.execute( + ApplicationProvider.getApplicationContext(), + message.parts.first(), + output, + false + ) + + // WHEN + val result = ExportMmsPartsUseCase.execute( + ApplicationProvider.getApplicationContext(), + message.parts.first(), + output, + false + ) + + // THEN + result.either( + onSuccess = { + validateExportedPart(message.parts.first(), output.messageId, expectedRowCount = 2) + }, + onFailure = { + throw it + } + ) + } + + private fun validateExportedPart( + part: ExportableMessage.Mms.Part, + messageId: Long, + expectedRowCount: Int = 1 + ) { + val context: Context = ApplicationProvider.getApplicationContext() + val baseUri: Uri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath("part").build() + val contentId = ExportMmsPartsUseCase.getContentId(part) + + context.contentResolver.query( + baseUri, + null, + "${Telephony.Mms.Part.CONTENT_ID} = ?", + arrayOf(contentId), + null, + null + )?.use { + it.moveToFirst() + assertEquals(expectedRowCount, it.count) + assertEquals(part.contentType, CursorUtil.requireString(it, Telephony.Mms.Part.CONTENT_TYPE)) + assertEquals(contentId, CursorUtil.requireString(it, Telephony.Mms.Part.CONTENT_ID)) + assertEquals(messageId, CursorUtil.requireLong(it, Telephony.Mms.Part.MSG_ID)) + assertEquals(if (part is ExportableMessage.Mms.Part.Text) part.text else null, CursorUtil.requireString(it, Telephony.Mms.Part.TEXT)) + } ?: org.junit.Assert.fail("Content Resolver returned a null cursor") + } +} diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsRecipientsUseCaseTest.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsRecipientsUseCaseTest.kt new file mode 100644 index 000000000..277d14c23 --- /dev/null +++ b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/ExportMmsRecipientsUseCaseTest.kt @@ -0,0 +1,138 @@ +package org.signal.smsexporter.internal.mms + +import android.content.Context +import android.net.Uri +import android.provider.Telephony +import androidx.test.core.app.ApplicationProvider +import com.google.android.mms.pdu_alt.PduHeaders +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.signal.core.util.CursorUtil +import org.signal.smsexporter.TestUtils + +@RunWith(RobolectricTestRunner::class) +class ExportMmsRecipientsUseCaseTest { + + @Before + fun setUp() { + TestUtils.setUpMmsContentProviderAndResolver() + } + + @Test + fun `When I export recipient, then I expect a valid exported recipient`() { + // GIVEN + val address = "+15065550177" + val sender = "+15065550123" + val messageId = 1L + + // WHEN + val result = ExportMmsRecipientsUseCase.execute( + ApplicationProvider.getApplicationContext(), + messageId, + address, + sender, + false + ) + + // THEN + result.either( + onSuccess = { + validateExportedRecipient(address, sender, messageId) + }, + onFailure = { + throw it + } + ) + } + + @Test + fun `Given recipient already exported, When I export recipient with check, then I expect a single exported recipient`() { + // GIVEN + val address = "+15065550177" + val sender = "+15065550123" + val messageId = 1L + ExportMmsRecipientsUseCase.execute( + ApplicationProvider.getApplicationContext(), + messageId, + address, + sender, + false + ) + + // WHEN + val result = ExportMmsRecipientsUseCase.execute( + ApplicationProvider.getApplicationContext(), + messageId, + address, + sender, + true + ) + + // THEN + result.either( + onSuccess = { + validateExportedRecipient(address, sender, messageId) + }, + onFailure = { + throw it + } + ) + } + + @Test + fun `Given recipient already exported, When I export recipient with check, then I expect a duplicate exported recipient`() { + // GIVEN + val address = "+15065550177" + val sender = "+15065550123" + val messageId = 1L + ExportMmsRecipientsUseCase.execute( + ApplicationProvider.getApplicationContext(), + messageId, + address, + sender, + false + ) + + // WHEN + val result = ExportMmsRecipientsUseCase.execute( + ApplicationProvider.getApplicationContext(), + messageId, + address, + sender, + false + ) + + // THEN + result.either( + onSuccess = { + validateExportedRecipient(address, sender, messageId, expectedRowCount = 2) + }, + onFailure = { + throw it + } + ) + } + + private fun validateExportedRecipient(address: String, sender: String, messageId: Long, expectedRowCount: Int = 1) { + val context: Context = ApplicationProvider.getApplicationContext() + val baseUri: Uri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(messageId.toString()).appendPath("addr").build() + + context.contentResolver.query( + baseUri, + null, + "${Telephony.Mms.Addr.ADDRESS} = ?", + arrayOf(address), + null, + null + )?.use { + it.moveToFirst() + assertEquals(expectedRowCount, it.count) + assertEquals(address, CursorUtil.requireString(it, Telephony.Mms.Addr.ADDRESS)) + assertEquals(if (address == sender) PduHeaders.FROM else PduHeaders.TO, CursorUtil.requireInt(it, Telephony.Mms.Addr.TYPE)) + } ?: fail("Content Resolver returned a null cursor") + } +} diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCaseTest.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCaseTest.kt new file mode 100644 index 000000000..abaaec740 --- /dev/null +++ b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/mms/GetOrCreateMmsThreadIdsUseCaseTest.kt @@ -0,0 +1,42 @@ +package org.signal.smsexporter.internal.mms + +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.signal.smsexporter.TestUtils + +@RunWith(RobolectricTestRunner::class) +class GetOrCreateMmsThreadIdsUseCaseTest { + + @Before + fun setUp() { + TestUtils.setUpMmsContentProviderAndResolver() + } + + @Test + fun `Given a message, when I execute, then I update the cache with the thread id`() { + // GIVEN + val mms = TestUtils.generateMmsMessage() + val threadCache = mutableMapOf, Long>() + + // WHEN + val result = GetOrCreateMmsThreadIdsUseCase.execute( + ApplicationProvider.getApplicationContext(), + mms, + threadCache + ) + + // THEN + result.either( + onSuccess = { + assertEquals(threadCache[mms.addresses], it.threadId) + }, + onFailure = { + throw it + } + ) + } +} diff --git a/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCaseTest.kt b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCaseTest.kt new file mode 100644 index 000000000..042d473d2 --- /dev/null +++ b/sms-exporter/lib/src/test/java/org/signal/smsexporter/internal/sms/ExportSmsMessagesUseCaseTest.kt @@ -0,0 +1,127 @@ +package org.signal.smsexporter.internal.sms + +import android.content.Context +import android.net.Uri +import android.provider.Telephony +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.signal.core.util.CursorUtil +import org.signal.smsexporter.ExportableMessage +import org.signal.smsexporter.TestUtils + +@RunWith(RobolectricTestRunner::class) +class ExportSmsMessagesUseCaseTest { + + @Before + fun setUp() { + TestUtils.setUpSmsContentProviderAndResolver() + } + + @Test + fun `Given an SMS message, when I execute, then I expect a record to be inserted into the SMS database`() { + // GIVEN + val exportableSmsMessage = TestUtils.generateSmsMessage() + + // WHEN + val result = ExportSmsMessagesUseCase.execute( + context = ApplicationProvider.getApplicationContext(), + sms = exportableSmsMessage, + checkForExistence = false + ) + + // THEN + result.either( + onSuccess = { + validateExportedMessage(exportableSmsMessage) + }, + onFailure = { + throw it + } + ) + } + + @Test + fun `Given an SMS message that already exists, when I execute and check for existence, then I expect only a single record to be inserted into the SMS database`() { + // GIVEN + val exportableSmsMessage = TestUtils.generateSmsMessage() + ExportSmsMessagesUseCase.execute( + context = ApplicationProvider.getApplicationContext(), + sms = exportableSmsMessage, + checkForExistence = false + ) + + // WHEN + val result = ExportSmsMessagesUseCase.execute( + context = ApplicationProvider.getApplicationContext(), + sms = exportableSmsMessage, + checkForExistence = true + ) + + // THEN + result.either( + onSuccess = { + validateExportedMessage(exportableSmsMessage) + }, + onFailure = { + throw it + } + ) + } + + @Test + fun `Given an SMS message that already exists, when I execute and do not check for existence, then I expect only a duplicate record to be inserted into the SMS database`() { + // GIVEN + val exportableSmsMessage = TestUtils.generateSmsMessage() + ExportSmsMessagesUseCase.execute( + context = ApplicationProvider.getApplicationContext(), + sms = exportableSmsMessage, + checkForExistence = false + ) + + // WHEN + val result = ExportSmsMessagesUseCase.execute( + context = ApplicationProvider.getApplicationContext(), + sms = exportableSmsMessage, + checkForExistence = false + ) + + // THEN + result.either( + onSuccess = { + validateExportedMessage(exportableSmsMessage, expectedRowCount = 2) + }, + onFailure = { + throw it + } + ) + } + + private fun validateExportedMessage(sms: ExportableMessage.Sms, expectedRowCount: Int = 1) { + // 1. Grab the SMS record from the content resolver + val context: Context = ApplicationProvider.getApplicationContext() + val baseUri: Uri = Telephony.Sms.CONTENT_URI + + context.contentResolver.query( + baseUri, + null, + "${Telephony.Sms.ADDRESS} = ? AND ${Telephony.Sms.DATE_SENT} = ?", + arrayOf(sms.address, sms.dateSent.toString()), + null, + null + )?.use { + it.moveToFirst() + assertEquals(expectedRowCount, it.count) + assertEquals(sms.address, CursorUtil.requireString(it, Telephony.Sms.ADDRESS)) + assertEquals(sms.dateSent, CursorUtil.requireLong(it, Telephony.Sms.DATE_SENT)) + assertEquals(sms.dateReceived, CursorUtil.requireLong(it, Telephony.Sms.DATE)) + assertEquals(sms.isRead, CursorUtil.requireBoolean(it, Telephony.Sms.READ)) + assertEquals(sms.body, CursorUtil.requireString(it, Telephony.Sms.BODY)) + assertEquals(if (sms.isOutgoing) Telephony.Sms.MESSAGE_TYPE_SENT else Telephony.Sms.MESSAGE_TYPE_INBOX, CursorUtil.requireInt(it, Telephony.Sms.TYPE)) + } ?: Assert.fail("Content Resolver returned a null cursor") + } +}