Skip to content

Commit

Permalink
SMS Exporter unit testing.
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-signal authored and greyson-signal committed Aug 24, 2022
1 parent 372f939 commit 777a91a
Show file tree
Hide file tree
Showing 11 changed files with 757 additions and 4 deletions.
8 changes: 8 additions & 0 deletions sms-exporter/lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ 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,
checkForExistence: Boolean
): Try<Output> {
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.")
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ internal object ExportMmsPartsUseCase {

private val TAG = Log.tag(ExportMmsPartsUseCase::class.java)

internal fun getContentId(part: ExportableMessage.Mms.Part): String {
return "<signal:${part.contentId}>"
}

fun execute(context: Context, part: ExportableMessage.Mms.Part, output: ExportMmsMessagesUseCase.Output, checkForExistence: Boolean): Try<Output> {
try {
val (message, messageId) = output
val contentId = "<signal:${part.contentId}>"
val contentId = getContentId(part)
val mmsPartUri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(messageId.toString()).appendPath("part").build()

if (checkForExistence) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<out String>?, p2: String?, p3: Array<out String>?, 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<out String>?): Int {
return -1
}

override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array<out String>?): 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
}
}
46 changes: 46 additions & 0 deletions sms-exporter/lib/src/test/java/org/signal/smsexporter/TestUtils.kt
Original file line number Diff line number Diff line change
@@ -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<String> = setOf("+15555060177"),
dateReceived: Long = 2,
dateSent: Long = 1,
isRead: Boolean = false,
isOutgoing: Boolean = false,
parts: List<ExportableMessage.Mms.Part> = 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()
)
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
}

0 comments on commit 777a91a

Please sign in to comment.