Prevent group leave event from bumping conversation.

This commit is contained in:
Cody Henthorne
2021-09-16 12:36:00 -04:00
committed by Alex Hart
parent b4465953d8
commit 5e968eb831
20 changed files with 989 additions and 51 deletions

View File

@@ -0,0 +1,74 @@
package org.thoughtcrime.securesms.database
import android.app.Application
import android.database.sqlite.SQLiteDatabase
import androidx.test.core.app.ApplicationProvider
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types
import org.thoughtcrime.securesms.testing.TestDatabaseUtil
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class MmsDatabaseTest {
private lateinit var db: SQLiteDatabase
private lateinit var mmsDatabase: MmsDatabase
@Before
fun setup() {
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
execSQL(MmsDatabase.CREATE_TABLE)
}
db = sqlCipher.writableDatabase
mmsDatabase = MmsDatabase(ApplicationProvider.getApplicationContext(), sqlCipher)
}
@After
fun tearDown() {
db.close()
}
@Test
fun `isGroupQuitMessage when normal message, return false`() {
val id = TestMms.insertMmsMessage(db, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT)
assertFalse(mmsDatabase.isGroupQuitMessage(id))
}
@Test
fun `isGroupQuitMessage when legacy quit message, return true`() {
val id = TestMms.insertMmsMessage(db, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT or Types.GROUP_LEAVE_BIT)
assertTrue(mmsDatabase.isGroupQuitMessage(id))
}
@Test
fun `isGroupQuitMessage when GV2 leave update, return false`() {
val id = TestMms.insertMmsMessage(db, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT or Types.GROUP_LEAVE_BIT or Types.GROUP_V2_BIT or Types.GROUP_UPDATE_BIT)
assertFalse(mmsDatabase.isGroupQuitMessage(id))
}
@Test
fun `getLatestGroupQuitTimestamp when only normal message, return -1`() {
TestMms.insertMmsMessage(db, threadId = 1, sentTimeMillis = 1, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT)
assertEquals(-1, mmsDatabase.getLatestGroupQuitTimestamp(1, 4))
}
@Test
fun `getLatestGroupQuitTimestamp when legacy quit, return message timestamp`() {
TestMms.insertMmsMessage(db, threadId = 1, sentTimeMillis = 2, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT or Types.GROUP_LEAVE_BIT)
assertEquals(2, mmsDatabase.getLatestGroupQuitTimestamp(1, 4))
}
@Test
fun `getLatestGroupQuitTimestamp when GV2 leave update message, return -1`() {
TestMms.insertMmsMessage(db, threadId = 1, sentTimeMillis = 3, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT or Types.GROUP_LEAVE_BIT or Types.GROUP_V2_BIT or Types.GROUP_UPDATE_BIT)
assertEquals(-1, mmsDatabase.getLatestGroupQuitTimestamp(1, 4))
}
}

View File

@@ -0,0 +1,79 @@
package org.thoughtcrime.securesms.database
import android.app.Application
import android.database.sqlite.SQLiteDatabase
import androidx.test.core.app.ApplicationProvider
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.testing.TestDatabaseUtil
import org.thoughtcrime.securesms.util.CursorUtil
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class MmsSmsDatabaseTest {
private lateinit var mmsSmsDatabase: MmsSmsDatabase
private lateinit var db: SQLiteDatabase
@Before
fun setup() {
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
execSQL(MmsDatabase.CREATE_TABLE)
execSQL(SmsDatabase.CREATE_TABLE)
}
db = sqlCipher.writableDatabase
mmsSmsDatabase = MmsSmsDatabase(ApplicationProvider.getApplicationContext(), sqlCipher)
}
@After
fun tearDown() {
db.close()
}
@Test
fun `getConversationSnippet when single normal SMS, return SMS message id and transport as false`() {
TestSms.insertSmsMessage(db)
mmsSmsDatabase.getConversationSnippetCursor(1).use { cursor ->
cursor.moveToFirst()
assertEquals(1, CursorUtil.requireLong(cursor, MmsSmsColumns.ID))
assertFalse(CursorUtil.requireBoolean(cursor, MmsSmsDatabase.TRANSPORT))
}
}
@Test
fun `getConversationSnippet when single normal MMS, return MMS message id and transport as true`() {
TestMms.insertMmsMessage(db)
mmsSmsDatabase.getConversationSnippetCursor(1).use { cursor ->
cursor.moveToFirst()
assertEquals(1, CursorUtil.requireLong(cursor, MmsSmsColumns.ID))
assertTrue(CursorUtil.requireBoolean(cursor, MmsSmsDatabase.TRANSPORT))
}
}
@Test
fun `getConversationSnippet when single normal MMS then GV2 leave update message, return MMS message id and transport as true both times`() {
val timestamp = System.currentTimeMillis()
TestMms.insertMmsMessage(db, receivedTimestampMillis = timestamp + 2)
mmsSmsDatabase.getConversationSnippetCursor(1).use { cursor ->
cursor.moveToFirst()
assertEquals(1, CursorUtil.requireLong(cursor, MmsSmsColumns.ID))
assertTrue(CursorUtil.requireBoolean(cursor, MmsSmsDatabase.TRANSPORT))
}
TestSms.insertSmsMessage(db, receivedTimestampMillis = timestamp + 3, type = MmsSmsColumns.Types.BASE_SENDING_TYPE or MmsSmsColumns.Types.SECURE_MESSAGE_BIT or MmsSmsColumns.Types.PUSH_MESSAGE_BIT or MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS)
mmsSmsDatabase.getConversationSnippetCursor(1).use { cursor ->
cursor.moveToFirst()
assertEquals(1, CursorUtil.requireLong(cursor, MmsSmsColumns.ID))
assertTrue(CursorUtil.requireBoolean(cursor, MmsSmsDatabase.TRANSPORT))
}
}
}

View File

@@ -0,0 +1,78 @@
package org.thoughtcrime.securesms.database
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import org.hamcrest.MatcherAssert.assertThat
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.testing.TestDatabaseUtil
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
import org.hamcrest.CoreMatchers.`is` as isEqual
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class SmsDatabaseTest {
private lateinit var writeableDatabase: AndroidSQLiteDatabase
private lateinit var smsDatabase: SmsDatabase
@Before
fun setup() {
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
execSQL(SmsDatabase.CREATE_TABLE)
}
writeableDatabase = sqlCipher.writableDatabase
smsDatabase = SmsDatabase(ApplicationProvider.getApplicationContext(), sqlCipher)
}
@After
fun tearDown() {
writeableDatabase.close()
}
@Test
fun `getThreadIdForMessage when no message absent for id, return -1`() {
assertThat(smsDatabase.getThreadIdForMessage(1), isEqual(-1))
}
@Test
fun `getThreadIdForMessage when message present for id, return thread id`() {
TestSms.insertSmsMessage(db = writeableDatabase, threadId = 1)
assertThat(smsDatabase.getThreadIdForMessage(1), isEqual(1))
}
@Test
fun `hasMeaningfulMessage when no messages, return false`() {
assertFalse(smsDatabase.hasMeaningfulMessage(1))
}
@Test
fun `hasMeaningfulMessage when normal message, return true`() {
TestSms.insertSmsMessage(db = writeableDatabase, threadId = 1)
assertTrue(smsDatabase.hasMeaningfulMessage(1))
}
@Test
fun `hasMeaningfulMessage when empty and then with ignored types, always return false`() {
assertFalse(smsDatabase.hasMeaningfulMessage(1))
TestSms.insertSmsMessage(db = writeableDatabase, threadId = 1, type = SmsDatabase.IGNORABLE_TYPESMASK_WHEN_COUNTING)
assertFalse(smsDatabase.hasMeaningfulMessage(1))
TestSms.insertSmsMessage(db = writeableDatabase, threadId = 1, type = MmsSmsColumns.Types.PROFILE_CHANGE_TYPE)
assertFalse(smsDatabase.hasMeaningfulMessage(1))
TestSms.insertSmsMessage(db = writeableDatabase, threadId = 1, type = MmsSmsColumns.Types.CHANGE_NUMBER_TYPE)
assertFalse(smsDatabase.hasMeaningfulMessage(1))
TestSms.insertSmsMessage(db = writeableDatabase, threadId = 1, type = MmsSmsColumns.Types.BASE_INBOX_TYPE or MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS)
assertFalse(smsDatabase.hasMeaningfulMessage(1))
}
}

View File

@@ -0,0 +1,86 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import com.google.android.mms.pdu_alt.PduHeaders
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
/**
* Helper methods for inserting an MMS message into the MMS table.
*/
object TestMms {
fun insertMmsMessage(
db: SQLiteDatabase,
recipient: Recipient = Recipient.UNKNOWN,
body: String = "body",
sentTimeMillis: Long = System.currentTimeMillis(),
receivedTimestampMillis: Long = System.currentTimeMillis(),
subscriptionId: Int = -1,
expiresIn: Long = 0,
viewOnce: Boolean = false,
distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
unread: Boolean = false,
threadId: Long = 1
): Long {
val message = OutgoingMediaMessage(
recipient,
body,
emptyList(),
sentTimeMillis,
subscriptionId,
expiresIn,
viewOnce,
distributionType,
null,
emptyList(),
emptyList(),
emptyList(),
emptyList(),
emptyList()
)
return insertMmsMessage(
db = db,
message = message,
body = body,
type = type,
unread = unread,
threadId = threadId,
receivedTimestampMillis = receivedTimestampMillis
)
}
fun insertMmsMessage(
db: SQLiteDatabase,
message: OutgoingMediaMessage,
body: String = message.body,
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
unread: Boolean = false,
threadId: Long = 1,
receivedTimestampMillis: Long = System.currentTimeMillis(),
): Long {
val contentValues = ContentValues()
contentValues.put(MmsDatabase.DATE_SENT, message.sentTimeMillis)
contentValues.put(MmsDatabase.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ)
contentValues.put(MmsDatabase.MESSAGE_BOX, type)
contentValues.put(MmsSmsColumns.THREAD_ID, threadId)
contentValues.put(MmsSmsColumns.READ, if (unread) 0 else 1)
contentValues.put(MmsDatabase.DATE_RECEIVED, receivedTimestampMillis)
contentValues.put(MmsSmsColumns.SUBSCRIPTION_ID, message.subscriptionId)
contentValues.put(MmsSmsColumns.EXPIRES_IN, message.expiresIn)
contentValues.put(MmsDatabase.VIEW_ONCE, message.isViewOnce)
contentValues.put(MmsSmsColumns.RECIPIENT_ID, message.recipient.id.serialize())
contentValues.put(MmsSmsColumns.DELIVERY_RECEIPT_COUNT, 0)
contentValues.put(MmsSmsColumns.RECEIPT_TIMESTAMP, 0)
contentValues.put(MmsSmsColumns.BODY, body)
contentValues.put(MmsDatabase.PART_COUNT, 0)
contentValues.put(MmsDatabase.MENTIONS_SELF, 0)
return db.insert(MmsDatabase.TABLE_NAME, null, contentValues)
}
}

View File

@@ -0,0 +1,85 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.text.TextUtils
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.whispersystems.libsignal.util.guava.Optional
import java.util.UUID
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
/**
* Helper methods for inserting SMS messages into the SMS table.
*/
object TestSms {
fun insertSmsMessage(
db: AndroidSQLiteDatabase,
sender: RecipientId = RecipientId.from(1),
senderDeviceId: Int = 1,
sentTimestampMillis: Long = System.currentTimeMillis(),
serverTimestampMillis: Long = System.currentTimeMillis(),
receivedTimestampMillis: Long = System.currentTimeMillis(),
encodedBody: String = "encodedBody",
groupId: Optional<GroupId> = Optional.absent(),
expiresInMillis: Long = 0,
unidentified: Boolean = false,
serverGuid: String = UUID.randomUUID().toString(),
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
unread: Boolean = false,
threadId: Long = 1
): Long {
val message = IncomingTextMessage(
sender,
senderDeviceId,
sentTimestampMillis,
serverTimestampMillis,
receivedTimestampMillis,
encodedBody,
groupId,
expiresInMillis,
unidentified,
serverGuid
)
return insertSmsMessage(
db = db,
message = message,
type = type,
unread = unread,
threadId = threadId
)
}
fun insertSmsMessage(
db: AndroidSQLiteDatabase,
message: IncomingTextMessage,
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
unread: Boolean = false,
threadId: Long = 1
): Long {
val values = ContentValues()
values.put(MmsSmsColumns.RECIPIENT_ID, message.sender.serialize())
values.put(MmsSmsColumns.ADDRESS_DEVICE_ID, message.senderDeviceId)
values.put(SmsDatabase.DATE_RECEIVED, message.receivedTimestampMillis)
values.put(SmsDatabase.DATE_SENT, message.sentTimestampMillis)
values.put(MmsSmsColumns.DATE_SERVER, message.serverTimestampMillis)
values.put(SmsDatabase.PROTOCOL, message.protocol)
values.put(MmsSmsColumns.READ, if (unread) 0 else 1)
values.put(MmsSmsColumns.SUBSCRIPTION_ID, message.subscriptionId)
values.put(MmsSmsColumns.EXPIRES_IN, message.expiresIn)
values.put(MmsSmsColumns.UNIDENTIFIED, message.isUnidentified)
if (!TextUtils.isEmpty(message.pseudoSubject)) values.put(SmsDatabase.SUBJECT, message.pseudoSubject)
values.put(SmsDatabase.REPLY_PATH_PRESENT, message.isReplyPathPresent)
values.put(SmsDatabase.SERVICE_CENTER, message.serviceCenterAddress)
values.put(MmsSmsColumns.BODY, message.messageBody)
values.put(SmsDatabase.TYPE, type)
values.put(MmsSmsColumns.THREAD_ID, threadId)
values.put(MmsSmsColumns.SERVER_GUID, message.serverGuid)
return db.insert(SmsDatabase.TABLE_NAME, null, values)
}
}

View File

@@ -0,0 +1,99 @@
package org.thoughtcrime.securesms.sms;
import androidx.annotation.NonNull;
import com.google.protobuf.ByteString;
import org.junit.Test;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.groups.v2.ChangeBuilder;
import org.thoughtcrime.securesms.mms.MessageGroupContext;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.util.Random;
import java.util.UUID;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class GroupV2UpdateMessageUtilTest {
@Test
public void isJustAGroupLeave_whenEditorIsRemoved_shouldReturnTrue() {
// GIVEN
UUID alice = UUID.randomUUID();
DecryptedGroupChange change = ChangeBuilder.changeBy(alice)
.deleteMember(alice)
.build();
DecryptedGroupV2Context context = DecryptedGroupV2Context.newBuilder()
.setContext(SignalServiceProtos.GroupContextV2.newBuilder()
.setMasterKey(ByteString.copyFrom(randomBytes())))
.setChange(change)
.build();
MessageGroupContext messageGroupContext = new MessageGroupContext(context);
// WHEN
boolean isJustAGroupLeave = GroupV2UpdateMessageUtil.isJustAGroupLeave(messageGroupContext);
// THEN
assertTrue(isJustAGroupLeave);
}
@Test
public void isJustAGroupLeave_whenOtherIsRemoved_shouldReturnFalse() {
// GIVEN
UUID alice = UUID.randomUUID();
UUID bob = UUID.randomUUID();
DecryptedGroupChange change = ChangeBuilder.changeBy(alice)
.deleteMember(bob)
.build();
DecryptedGroupV2Context context = DecryptedGroupV2Context.newBuilder()
.setContext(SignalServiceProtos.GroupContextV2.newBuilder()
.setMasterKey(ByteString.copyFrom(randomBytes())))
.setChange(change)
.build();
MessageGroupContext messageGroupContext = new MessageGroupContext(context);
// WHEN
boolean isJustAGroupLeave = GroupV2UpdateMessageUtil.isJustAGroupLeave(messageGroupContext);
// THEN
assertFalse(isJustAGroupLeave);
}
@Test
public void isJustAGroupLeave_whenEditorIsRemovedAndOtherChanges_shouldReturnFalse() {
// GIVEN
UUID alice = UUID.randomUUID();
UUID bob = UUID.randomUUID();
DecryptedGroupChange change = ChangeBuilder.changeBy(alice)
.deleteMember(alice)
.addMember(bob)
.build();
DecryptedGroupV2Context context = DecryptedGroupV2Context.newBuilder()
.setContext(SignalServiceProtos.GroupContextV2.newBuilder()
.setMasterKey(ByteString.copyFrom(randomBytes())))
.setChange(change)
.build();
MessageGroupContext messageGroupContext = new MessageGroupContext(context);
// WHEN
boolean isJustAGroupLeave = GroupV2UpdateMessageUtil.isJustAGroupLeave(messageGroupContext);
// THEN
assertFalse(isJustAGroupLeave);
}
private @NonNull byte[] randomBytes() {
byte[] bytes = new byte[32];
new Random().nextBytes(bytes);
return bytes;
}
}

View File

@@ -0,0 +1,89 @@
package org.thoughtcrime.securesms.testing
import android.content.Context
import org.thoughtcrime.securesms.crypto.DatabaseSecret
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import java.security.SecureRandom
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
import net.zetetic.database.sqlcipher.SQLiteDatabase as SQLCipherSQLiteDatabase
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSQLiteDatabase
/**
* Proxy [SQLCipherOpenHelper] to the [TestSQLiteOpenHelper] interface.
*/
class ProxySQLCipherOpenHelper(
context: Context,
val readableDatabase: AndroidSQLiteDatabase,
val writableDatabase: AndroidSQLiteDatabase,
) : SQLCipherOpenHelper(context, DatabaseSecret(ByteArray(32).apply { SecureRandom().nextBytes(this) })) {
constructor(context: Context, testOpenHelper: TestSQLiteOpenHelper) : this(context, testOpenHelper.readableDatabase, testOpenHelper.writableDatabase)
override fun close() {
throw UnsupportedOperationException()
}
override fun getDatabaseName(): String {
throw UnsupportedOperationException()
}
override fun setWriteAheadLoggingEnabled(enabled: Boolean) {
throw UnsupportedOperationException()
}
override fun onConfigure(db: SQLCipherSQLiteDatabase) {
throw UnsupportedOperationException()
}
override fun onBeforeDelete(db: SQLCipherSQLiteDatabase?) {
throw UnsupportedOperationException()
}
override fun onDowngrade(db: SQLCipherSQLiteDatabase?, oldVersion: Int, newVersion: Int) {
throw UnsupportedOperationException()
}
override fun onOpen(db: SQLCipherSQLiteDatabase?) {
throw UnsupportedOperationException()
}
override fun onCreate(db: SQLCipherSQLiteDatabase?) {
throw UnsupportedOperationException()
}
override fun onUpgrade(db: SQLCipherSQLiteDatabase?, oldVersion: Int, newVersion: Int) {
throw UnsupportedOperationException()
}
override fun getReadableDatabase(): SQLCipherSQLiteDatabase {
throw UnsupportedOperationException()
}
override fun getWritableDatabase(): SQLCipherSQLiteDatabase {
throw UnsupportedOperationException()
}
override fun getRawReadableDatabase(): SQLCipherSQLiteDatabase {
throw UnsupportedOperationException()
}
override fun getRawWritableDatabase(): SQLCipherSQLiteDatabase {
throw UnsupportedOperationException()
}
override fun getSignalReadableDatabase(): SignalSQLiteDatabase {
return ProxySignalSQLiteDatabase(readableDatabase)
}
override fun getSignalWritableDatabase(): SignalSQLiteDatabase {
return ProxySignalSQLiteDatabase(writableDatabase)
}
override fun getSqlCipherDatabase(): SQLCipherSQLiteDatabase {
throw UnsupportedOperationException()
}
override fun markCurrent(db: SQLCipherSQLiteDatabase?) {
throw UnsupportedOperationException()
}
}

View File

@@ -0,0 +1,236 @@
package org.thoughtcrime.securesms.testing
import android.content.ContentValues
import android.database.Cursor
import java.util.Locale
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
import android.database.sqlite.SQLiteTransactionListener as AndroidSQLiteTransactionListener
import net.zetetic.database.sqlcipher.SQLiteStatement as SQLCipherSQLiteStatement
import net.zetetic.database.sqlcipher.SQLiteTransactionListener as SQLCipherSQLiteTransactionListener
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSQLiteDatabase
/**
* Partial implementation of [SignalSQLiteDatabase] using an instance of [AndroidSQLiteDatabase] instead
* of SQLCipher.
*/
class ProxySignalSQLiteDatabase(private val database: AndroidSQLiteDatabase) : SignalSQLiteDatabase(null) {
override fun getSqlCipherDatabase(): net.zetetic.database.sqlcipher.SQLiteDatabase {
throw UnsupportedOperationException()
}
override fun beginTransaction() {
database.beginTransaction()
}
override fun endTransaction() {
database.endTransaction()
}
override fun setTransactionSuccessful() {
database.setTransactionSuccessful()
}
override fun query(distinct: Boolean, table: String?, columns: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, groupBy: String?, having: String?, orderBy: String?, limit: String?): Cursor {
return database.query(distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit)
}
override fun queryWithFactory(
cursorFactory: net.zetetic.database.sqlcipher.SQLiteDatabase.CursorFactory?,
distinct: Boolean,
table: String?,
columns: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
groupBy: String?,
having: String?,
orderBy: String?,
limit: String?
): Cursor {
return database.queryWithFactory(null, distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit)
}
override fun query(table: String?, columns: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, groupBy: String?, having: String?, orderBy: String?): Cursor {
return database.query(table, columns, selection, selectionArgs, groupBy, having, orderBy)
}
override fun query(table: String?, columns: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, groupBy: String?, having: String?, orderBy: String?, limit: String?): Cursor {
return database.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit)
}
override fun rawQuery(sql: String?, selectionArgs: Array<out String>?): Cursor {
return database.rawQuery(sql, selectionArgs)
}
override fun rawQuery(sql: String?, args: Array<out Any>?): Cursor {
return database.rawQuery(sql, args?.map(Any::toString)?.toTypedArray())
}
override fun rawQueryWithFactory(cursorFactory: net.zetetic.database.sqlcipher.SQLiteDatabase.CursorFactory?, sql: String?, selectionArgs: Array<out String>?, editTable: String?): Cursor {
return database.rawQueryWithFactory(null, sql, selectionArgs, editTable)
}
override fun rawQuery(sql: String?, selectionArgs: Array<out String>?, initialRead: Int, maxRead: Int): Cursor {
throw UnsupportedOperationException()
}
override fun insert(table: String?, nullColumnHack: String?, values: ContentValues?): Long {
return database.insert(table, nullColumnHack, values)
}
override fun insertOrThrow(table: String?, nullColumnHack: String?, values: ContentValues?): Long {
return database.insertOrThrow(table, nullColumnHack, values)
}
override fun replace(table: String?, nullColumnHack: String?, initialValues: ContentValues?): Long {
return database.replace(table, nullColumnHack, initialValues)
}
override fun replaceOrThrow(table: String?, nullColumnHack: String?, initialValues: ContentValues?): Long {
return database.replaceOrThrow(table, nullColumnHack, initialValues)
}
override fun insertWithOnConflict(table: String?, nullColumnHack: String?, initialValues: ContentValues?, conflictAlgorithm: Int): Long {
return database.insertWithOnConflict(table, nullColumnHack, initialValues, conflictAlgorithm)
}
override fun delete(table: String?, whereClause: String?, whereArgs: Array<out String>?): Int {
return database.delete(table, whereClause, whereArgs)
}
override fun update(table: String?, values: ContentValues?, whereClause: String?, whereArgs: Array<out String>?): Int {
return database.update(table, values, whereClause, whereArgs)
}
override fun updateWithOnConflict(table: String?, values: ContentValues?, whereClause: String?, whereArgs: Array<out String>?, conflictAlgorithm: Int): Int {
return database.updateWithOnConflict(table, values, whereClause, whereArgs, conflictAlgorithm)
}
override fun execSQL(sql: String?) {
database.execSQL(sql)
}
override fun rawExecSQL(sql: String?) {
database.execSQL(sql)
}
override fun execSQL(sql: String?, bindArgs: Array<out Any>?) {
database.execSQL(sql, bindArgs)
}
override fun enableWriteAheadLogging(): Boolean {
throw UnsupportedOperationException()
}
override fun disableWriteAheadLogging() {
throw UnsupportedOperationException()
}
override fun isWriteAheadLoggingEnabled(): Boolean {
throw UnsupportedOperationException()
}
override fun setForeignKeyConstraintsEnabled(enable: Boolean) {
database.setForeignKeyConstraintsEnabled(enable)
}
override fun beginTransactionWithListener(transactionListener: SQLCipherSQLiteTransactionListener?) {
database.beginTransactionWithListener(object : AndroidSQLiteTransactionListener {
override fun onBegin() {
transactionListener?.onBegin()
}
override fun onCommit() {
transactionListener?.onCommit()
}
override fun onRollback() {
transactionListener?.onRollback()
}
})
}
override fun beginTransactionNonExclusive() {
database.beginTransactionNonExclusive()
}
override fun beginTransactionWithListenerNonExclusive(transactionListener: SQLCipherSQLiteTransactionListener?) {
database.beginTransactionWithListenerNonExclusive(object : AndroidSQLiteTransactionListener {
override fun onBegin() {
transactionListener?.onBegin()
}
override fun onCommit() {
transactionListener?.onCommit()
}
override fun onRollback() {
transactionListener?.onRollback()
}
})
}
override fun inTransaction(): Boolean {
return database.inTransaction()
}
override fun isDbLockedByCurrentThread(): Boolean {
return database.isDbLockedByCurrentThread
}
@Suppress("DEPRECATION")
override fun isDbLockedByOtherThreads(): Boolean {
return database.isDbLockedByOtherThreads
}
override fun yieldIfContendedSafely(): Boolean {
return database.yieldIfContendedSafely()
}
override fun yieldIfContendedSafely(sleepAfterYieldDelay: Long): Boolean {
return database.yieldIfContendedSafely(sleepAfterYieldDelay)
}
override fun getVersion(): Int {
return database.version
}
override fun setVersion(version: Int) {
database.version = version
}
override fun getMaximumSize(): Long {
return database.maximumSize
}
override fun setMaximumSize(numBytes: Long): Long {
return database.setMaximumSize(numBytes)
}
override fun getPageSize(): Long {
return database.pageSize
}
override fun setPageSize(numBytes: Long) {
database.pageSize = numBytes
}
override fun compileStatement(sql: String?): SQLCipherSQLiteStatement {
throw UnsupportedOperationException()
}
override fun isReadOnly(): Boolean {
return database.isReadOnly
}
override fun isOpen(): Boolean {
return database.isOpen
}
override fun needUpgrade(newVersion: Int): Boolean {
return database.needUpgrade(newVersion)
}
override fun setLocale(locale: Locale?) {
database.setLocale(locale)
}
}

View File

@@ -0,0 +1,30 @@
package org.thoughtcrime.securesms.testing
import android.database.sqlite.SQLiteDatabase
import androidx.test.core.app.ApplicationProvider
import java.io.File
/**
* Helper for creating/reading a database for unit tests.
*/
object TestDatabaseUtil {
/**
* Create an in-memory only database that is empty. Can pass [onCreate] to do similar operations
* one would do in a open helper's onCreate.
*/
fun inMemoryDatabase(onCreate: OnCreate): ProxySQLCipherOpenHelper {
val testSQLiteOpenHelper = TestSQLiteOpenHelper(ApplicationProvider.getApplicationContext(), onCreate)
return ProxySQLCipherOpenHelper(ApplicationProvider.getApplicationContext(), testSQLiteOpenHelper)
}
/**
* Open a database file located in app/src/test/resources/db. Currently only reads
* are allowed due to weird caching of the file resulting in non-deterministic tests.
*/
fun fromFileDatabase(name: String): ProxySQLCipherOpenHelper {
val databaseFile = File(javaClass.getResource("/db/$name")!!.file)
val sqliteDatabase = SQLiteDatabase.openDatabase(databaseFile.absolutePath, null, SQLiteDatabase.OPEN_READONLY)
return ProxySQLCipherOpenHelper(ApplicationProvider.getApplicationContext(), sqliteDatabase, sqliteDatabase)
}
}

View File

@@ -0,0 +1,25 @@
package org.thoughtcrime.securesms.testing
import android.content.Context
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper as AndroidSQLiteOpenHelper
typealias OnCreate = AndroidSQLiteDatabase.() -> Unit
/**
* [AndroidSQLiteOpenHelper] for use in unit tests.
*/
class TestSQLiteOpenHelper(context: Context, private val onCreate: OnCreate) : AndroidSQLiteOpenHelper(context, "test", null, 1) {
fun setup() {
onCreate(writableDatabase)
}
override fun onCreate(db: AndroidSQLiteDatabase) {
onCreate.invoke(db)
}
override fun onUpgrade(db: AndroidSQLiteDatabase, oldVersion: Int, newVersion: Int) {
// no upgrade
}
}