SMS Exporter unit testing.

fork-5.53.8
Alex Hart 2022-08-24 16:43:18 -03:00 zatwierdzone przez Greyson Parrelli
rodzic 372f939a67
commit 777a91abc7
11 zmienionych plików z 757 dodań i 4 usunięć

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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<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.")
@ -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)

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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)

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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()
)
}
}

Wyświetl plik

@ -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")
}
}

Wyświetl plik

@ -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")
}
}

Wyświetl plik

@ -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")
}
}

Wyświetl plik

@ -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<Set<String>, Long>()
// WHEN
val result = GetOrCreateMmsThreadIdsUseCase.execute(
ApplicationProvider.getApplicationContext(),
mms,
threadCache
)
// THEN
result.either(
onSuccess = {
assertEquals(threadCache[mms.addresses], it.threadId)
},
onFailure = {
throw it
}
)
}
}

Wyświetl plik

@ -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")
}
}