Migrate to shared class hierarchy for unit based database tests.

This commit is contained in:
Cody Henthorne
2025-02-28 09:31:19 -05:00
committed by Greyson Parrelli
parent d5e18a8bd5
commit d0b6d6fdeb
138 changed files with 394 additions and 436 deletions

View File

@@ -1,114 +1,96 @@
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.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.testing.TestDatabaseUtil
import org.thoughtcrime.securesms.testutil.SignalDatabaseRule
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class MmsDatabaseTest {
private lateinit var db: SQLiteDatabase
private lateinit var messageTable: MessageTable
@Before
fun setup() {
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
execSQL(MessageTable.CREATE_TABLE)
}
db = sqlCipher.myWritableDatabase
messageTable = MessageTable(ApplicationProvider.getApplicationContext(), sqlCipher)
}
@After
fun tearDown() {
db.close()
}
@get:Rule
val signalDatabaseRule = SignalDatabaseRule()
@Test
fun `isGroupQuitMessage when normal message, return false`() {
val id = TestMms.insert(db, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT)
assertFalse(messageTable.isGroupQuitMessage(id))
val id = TestMms.insert(signalDatabaseRule.writeableDatabase, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT)
assertFalse(SignalDatabase.messages.isGroupQuitMessage(id))
}
@Test
fun `isGroupQuitMessage when legacy quit message, return true`() {
val id = TestMms.insert(db, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.GROUP_LEAVE_BIT)
assertTrue(messageTable.isGroupQuitMessage(id))
val id = TestMms.insert(signalDatabaseRule.writeableDatabase, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.GROUP_LEAVE_BIT)
assertTrue(SignalDatabase.messages.isGroupQuitMessage(id))
}
@Test
fun `isGroupQuitMessage when GV2 leave update, return false`() {
val id = TestMms.insert(db, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.GROUP_LEAVE_BIT or MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT)
assertFalse(messageTable.isGroupQuitMessage(id))
val id = TestMms.insert(signalDatabaseRule.writeableDatabase, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.GROUP_LEAVE_BIT or MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT)
assertFalse(SignalDatabase.messages.isGroupQuitMessage(id))
}
@Test
fun `getLatestGroupQuitTimestamp when only normal message, return -1`() {
TestMms.insert(db, threadId = 1, sentTimeMillis = 1, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT)
assertEquals(-1, messageTable.getLatestGroupQuitTimestamp(1, 4))
TestMms.insert(signalDatabaseRule.writeableDatabase, threadId = 1, sentTimeMillis = 1, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT)
assertEquals(-1, SignalDatabase.messages.getLatestGroupQuitTimestamp(1, 4))
}
@Test
fun `getLatestGroupQuitTimestamp when legacy quit, return message timestamp`() {
TestMms.insert(db, threadId = 1, sentTimeMillis = 2, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.GROUP_LEAVE_BIT)
assertEquals(2, messageTable.getLatestGroupQuitTimestamp(1, 4))
TestMms.insert(signalDatabaseRule.writeableDatabase, threadId = 1, sentTimeMillis = 2, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.GROUP_LEAVE_BIT)
assertEquals(2, SignalDatabase.messages.getLatestGroupQuitTimestamp(1, 4))
}
@Test
fun `getLatestGroupQuitTimestamp when GV2 leave update message, return -1`() {
TestMms.insert(db, threadId = 1, sentTimeMillis = 3, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.GROUP_LEAVE_BIT or MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT)
assertEquals(-1, messageTable.getLatestGroupQuitTimestamp(1, 4))
TestMms.insert(signalDatabaseRule.writeableDatabase, threadId = 1, sentTimeMillis = 3, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.GROUP_LEAVE_BIT or MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT)
assertEquals(-1, SignalDatabase.messages.getLatestGroupQuitTimestamp(1, 4))
}
@Test
fun `Given no stories in database, when I getStoryViewState, then I expect NONE`() {
assertEquals(StoryViewState.NONE, messageTable.getStoryViewState(1))
assertEquals(StoryViewState.NONE, SignalDatabase.messages.getStoryViewState(1))
}
@Test
fun `Given stories in database not in thread 1, when I getStoryViewState for thread 1, then I expect NONE`() {
TestMms.insert(db, threadId = 2, storyType = StoryType.STORY_WITH_REPLIES)
TestMms.insert(db, threadId = 2, storyType = StoryType.STORY_WITH_REPLIES)
assertEquals(StoryViewState.NONE, messageTable.getStoryViewState(1))
TestMms.insert(signalDatabaseRule.writeableDatabase, threadId = 2, storyType = StoryType.STORY_WITH_REPLIES)
TestMms.insert(signalDatabaseRule.writeableDatabase, threadId = 2, storyType = StoryType.STORY_WITH_REPLIES)
assertEquals(StoryViewState.NONE, SignalDatabase.messages.getStoryViewState(1))
}
@Test
fun `Given viewed incoming stories in database, when I getStoryViewState, then I expect VIEWED`() {
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = true)
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = true)
assertEquals(StoryViewState.VIEWED, messageTable.getStoryViewState(1))
TestMms.insert(signalDatabaseRule.writeableDatabase, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = true)
TestMms.insert(signalDatabaseRule.writeableDatabase, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = true)
assertEquals(StoryViewState.VIEWED, SignalDatabase.messages.getStoryViewState(1))
}
@Test
fun `Given unviewed incoming stories in database, when I getStoryViewState, then I expect UNVIEWED`() {
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = false)
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = false)
assertEquals(StoryViewState.UNVIEWED, messageTable.getStoryViewState(1))
TestMms.insert(signalDatabaseRule.writeableDatabase, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = false)
TestMms.insert(signalDatabaseRule.writeableDatabase, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = false)
assertEquals(StoryViewState.UNVIEWED, SignalDatabase.messages.getStoryViewState(1))
}
@Test
fun `Given mix of viewed and unviewed incoming stories in database, when I getStoryViewState, then I expect UNVIEWED`() {
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = true)
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = false)
assertEquals(StoryViewState.UNVIEWED, messageTable.getStoryViewState(1))
TestMms.insert(signalDatabaseRule.writeableDatabase, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = true)
TestMms.insert(signalDatabaseRule.writeableDatabase, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = false)
assertEquals(StoryViewState.UNVIEWED, SignalDatabase.messages.getStoryViewState(1))
}
@Test
fun `Given only outgoing story in database, when I getStoryViewState, then I expect VIEWED`() {
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, type = MessageTypes.BASE_OUTBOX_TYPE)
assertEquals(StoryViewState.VIEWED, messageTable.getStoryViewState(1))
TestMms.insert(signalDatabaseRule.writeableDatabase, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, type = MessageTypes.BASE_OUTBOX_TYPE)
assertEquals(StoryViewState.VIEWED, SignalDatabase.messages.getStoryViewState(1))
}
}

View File

@@ -1,45 +1,26 @@
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.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.testing.TestDatabaseUtil
import org.thoughtcrime.securesms.testutil.SignalDatabaseRule
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class MmsSmsDatabaseTest {
private lateinit var messageTable: MessageTable
private lateinit var db: SQLiteDatabase
@Before
fun setup() {
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
execSQL(MessageTable.CREATE_TABLE)
MessageTable.CREATE_INDEXS.forEach { execSQL(it) }
}
db = sqlCipher.myWritableDatabase
messageTable = MessageTable(ApplicationProvider.getApplicationContext(), sqlCipher)
}
@After
fun tearDown() {
db.close()
}
@get:Rule
val signalDatabaseRule = SignalDatabaseRule()
@Test
fun `getConversationSnippet when single normal SMS, return SMS message id and transport as false`() {
TestSms.insert(db)
messageTable.getConversationSnippetCursor(1).use { cursor ->
TestSms.insert(signalDatabaseRule.writeableDatabase)
SignalDatabase.messages.getConversationSnippetCursor(1).use { cursor ->
cursor.moveToFirst()
assertEquals(1, CursorUtil.requireLong(cursor, MessageTable.ID))
}
@@ -47,8 +28,8 @@ class MmsSmsDatabaseTest {
@Test
fun `getConversationSnippet when single normal MMS, return MMS message id and transport as true`() {
TestMms.insert(db)
messageTable.getConversationSnippetCursor(1).use { cursor ->
TestMms.insert(signalDatabaseRule.writeableDatabase)
SignalDatabase.messages.getConversationSnippetCursor(1).use { cursor ->
cursor.moveToFirst()
assertEquals(1, CursorUtil.requireLong(cursor, MessageTable.ID))
}
@@ -58,14 +39,14 @@ class MmsSmsDatabaseTest {
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.insert(db, receivedTimestampMillis = timestamp + 2)
messageTable.getConversationSnippetCursor(1).use { cursor ->
TestMms.insert(signalDatabaseRule.writeableDatabase, receivedTimestampMillis = timestamp + 2)
SignalDatabase.messages.getConversationSnippetCursor(1).use { cursor ->
cursor.moveToFirst()
assertEquals(1, CursorUtil.requireLong(cursor, MessageTable.ID))
}
TestSms.insert(db, receivedTimestampMillis = timestamp + 3, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.GROUP_V2_LEAVE_BITS)
messageTable.getConversationSnippetCursor(1).use { cursor ->
TestSms.insert(signalDatabaseRule.writeableDatabase, receivedTimestampMillis = timestamp + 3, type = MessageTypes.BASE_SENDING_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.GROUP_V2_LEAVE_BITS)
SignalDatabase.messages.getConversationSnippetCursor(1).use { cursor ->
cursor.moveToFirst()
assertEquals(1, CursorUtil.requireLong(cursor, MessageTable.ID))
}

View File

@@ -1,16 +1,12 @@
package org.thoughtcrime.securesms.database
import android.app.Application
import android.database.sqlite.SQLiteDatabase
import androidx.test.core.app.ApplicationProvider
import assertk.assertThat
import assertk.assertions.containsExactlyInAnyOrder
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isTrue
import assertk.assertions.single
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -19,8 +15,8 @@ import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.TestDatabaseUtil
import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule
import org.thoughtcrime.securesms.testutil.SignalDatabaseRule
import java.time.DayOfWeek
@RunWith(RobolectricTestRunner::class)
@@ -29,34 +25,12 @@ class NotificationProfileTablesTest {
@get:Rule
val appDependencies = MockAppDependenciesRule()
private lateinit var db: SQLiteDatabase
private lateinit var database: NotificationProfileTables
@Before
fun setup() {
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
NotificationProfileTables.CREATE_TABLE.forEach {
println(it)
this.execSQL(it)
}
NotificationProfileTables.CREATE_INDEXES.forEach {
println(it)
this.execSQL(it)
}
}
db = sqlCipher.myWritableDatabase
database = NotificationProfileTables(ApplicationProvider.getApplicationContext(), sqlCipher)
}
@After
fun tearDown() {
db.close()
}
@get:Rule
val signalDatabaseRule = SignalDatabaseRule()
@Test
fun `addProfile for profile with empty schedule and members`() {
val profile: NotificationProfile = database.createProfile(
val profile: NotificationProfile = SignalDatabase.notificationProfiles.createProfile(
name = "Profile",
emoji = "avatar",
color = AvatarColor.A210,
@@ -69,7 +43,7 @@ class NotificationProfileTablesTest {
assertThat(profile.createdAt).isEqualTo(1000L)
assertThat(profile.schedule.id).isEqualTo(1)
val profiles = database.getProfiles()
val profiles = SignalDatabase.notificationProfiles.getProfiles()
assertThat(profiles)
.single()
@@ -84,14 +58,14 @@ class NotificationProfileTablesTest {
@Test
fun `updateProfile changes all updateable fields`() {
val profile: NotificationProfile = database.createProfile(
val profile: NotificationProfile = SignalDatabase.notificationProfiles.createProfile(
name = "Profile",
emoji = "avatar",
color = AvatarColor.A210,
createdAt = 1000L
).profile
val updatedProfile = database.updateProfile(
val updatedProfile = SignalDatabase.notificationProfiles.updateProfile(
profile.copy(
name = "Profile 2",
emoji = "avatar 2",
@@ -109,7 +83,7 @@ class NotificationProfileTablesTest {
@Test
fun `when allowed recipients change profile changes`() {
val profile: NotificationProfile = database.createProfile(
val profile: NotificationProfile = SignalDatabase.notificationProfiles.createProfile(
name = "Profile",
emoji = "avatar",
color = AvatarColor.A210,
@@ -117,22 +91,22 @@ class NotificationProfileTablesTest {
).profile
assertThat(profile.isRecipientAllowed(RecipientId.from(1))).isFalse()
var updated = database.addAllowedRecipient(profile.id, RecipientId.from(1))
var updated = SignalDatabase.notificationProfiles.addAllowedRecipient(profile.id, RecipientId.from(1))
assertThat(updated.isRecipientAllowed(RecipientId.from(1))).isTrue()
updated = database.removeAllowedRecipient(profile.id, RecipientId.from(1))
updated = SignalDatabase.notificationProfiles.removeAllowedRecipient(profile.id, RecipientId.from(1))
assertThat(updated.isRecipientAllowed(RecipientId.from(1))).isFalse()
updated = database.updateProfile(updated.copy(allowedMembers = setOf(RecipientId.from(1)))).profile
updated = SignalDatabase.notificationProfiles.updateProfile(updated.copy(allowedMembers = setOf(RecipientId.from(1)))).profile
assertThat(updated.isRecipientAllowed(RecipientId.from(1))).isTrue()
updated = database.updateProfile(updated.copy(allowedMembers = emptySet())).profile
updated = SignalDatabase.notificationProfiles.updateProfile(updated.copy(allowedMembers = emptySet())).profile
assertThat(updated.isRecipientAllowed(RecipientId.from(1))).isFalse()
}
@Test
fun `when schedule change profile changes`() {
val profile: NotificationProfile = database.createProfile(
val profile: NotificationProfile = SignalDatabase.notificationProfiles.createProfile(
name = "Profile",
emoji = "avatar",
color = AvatarColor.A210,
@@ -144,7 +118,7 @@ class NotificationProfileTablesTest {
assertThat(profile.schedule.daysEnabled, "Contains correct default days")
.containsExactlyInAnyOrder(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY)
database.updateSchedule(
SignalDatabase.notificationProfiles.updateSchedule(
profile.schedule.copy(
enabled = true,
start = 800,
@@ -152,22 +126,22 @@ class NotificationProfileTablesTest {
daysEnabled = setOf(DayOfWeek.SUNDAY, DayOfWeek.FRIDAY)
)
)
var updated = database.getProfile(profile.id)!!
var updated = SignalDatabase.notificationProfiles.getProfile(profile.id)!!
assertThat(updated.schedule.enabled).isTrue()
assertThat(updated.schedule.start).isEqualTo(800)
assertThat(updated.schedule.end).isEqualTo(1800)
assertThat(updated.schedule.daysEnabled, "Contains updated days days")
.containsExactlyInAnyOrder(DayOfWeek.SUNDAY, DayOfWeek.FRIDAY)
database.updateSchedule(profile.schedule)
updated = database.getProfile(profile.id)!!
SignalDatabase.notificationProfiles.updateSchedule(profile.schedule)
updated = SignalDatabase.notificationProfiles.getProfile(profile.id)!!
assertThat(updated.schedule.enabled).isFalse()
assertThat(updated.schedule.start).isEqualTo(900)
assertThat(updated.schedule.end).isEqualTo(1700)
assertThat(updated.schedule.daysEnabled, "Contains correct default days")
.containsExactlyInAnyOrder(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY)
updated = database.updateProfile(
updated = SignalDatabase.notificationProfiles.updateProfile(
profile.copy(
schedule = profile.schedule.copy(
enabled = true,
@@ -183,7 +157,7 @@ class NotificationProfileTablesTest {
assertThat(updated.schedule.daysEnabled, "Contains updated days days")
.containsExactlyInAnyOrder(DayOfWeek.SUNDAY, DayOfWeek.FRIDAY)
updated = database.updateProfile(profile).profile
updated = SignalDatabase.notificationProfiles.updateProfile(profile).profile
assertThat(updated.schedule.enabled).isFalse()
assertThat(updated.schedule.start).isEqualTo(900)
assertThat(updated.schedule.end).isEqualTo(1700)

View File

@@ -1,92 +1,71 @@
package org.thoughtcrime.securesms.database
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isTrue
import org.junit.After
import org.junit.Before
import org.junit.Rule
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.thoughtcrime.securesms.testutil.SignalDatabaseRule
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class SmsDatabaseTest {
private lateinit var db: AndroidSQLiteDatabase
private lateinit var messageTable: MessageTable
@Before
fun setup() {
val sqlCipher = TestDatabaseUtil.inMemoryDatabase {
execSQL(MessageTable.CREATE_TABLE)
MessageTable.CREATE_INDEXS.forEach {
execSQL(it)
}
}
db = sqlCipher.myWritableDatabase
messageTable = MessageTable(ApplicationProvider.getApplicationContext(), sqlCipher)
}
@After
fun tearDown() {
db.close()
}
@get:Rule
val signalDatabaseRule = SignalDatabaseRule()
@Test
fun `getThreadIdForMessage when no message absent for id, return -1`() {
assertThat(messageTable.getThreadIdForMessage(1)).isEqualTo(-1)
assertThat(SignalDatabase.messages.getThreadIdForMessage(1)).isEqualTo(-1)
}
@Test
fun `getThreadIdForMessage when message present for id, return thread id`() {
TestSms.insert(db)
assertThat(messageTable.getThreadIdForMessage(1)).isEqualTo(1)
TestSms.insert(signalDatabaseRule.writeableDatabase)
assertThat(SignalDatabase.messages.getThreadIdForMessage(1)).isEqualTo(1)
}
@Test
fun `hasMeaningfulMessage when no messages, return false`() {
assertThat(messageTable.hasMeaningfulMessage(1)).isFalse()
assertThat(SignalDatabase.messages.hasMeaningfulMessage(1)).isFalse()
}
@Test
fun `hasMeaningfulMessage when normal message, return true`() {
TestSms.insert(db)
assertThat(messageTable.hasMeaningfulMessage(1)).isTrue()
TestSms.insert(signalDatabaseRule.writeableDatabase)
assertThat(SignalDatabase.messages.hasMeaningfulMessage(1)).isTrue()
}
@Test
fun `hasMeaningfulMessage when GV2 create message only, return true`() {
TestSms.insert(db, type = MessageTypes.BASE_INBOX_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT)
assertThat(messageTable.hasMeaningfulMessage(1)).isTrue()
TestSms.insert(signalDatabaseRule.writeableDatabase, type = MessageTypes.BASE_INBOX_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT)
assertThat(SignalDatabase.messages.hasMeaningfulMessage(1)).isTrue()
}
@Test
fun `hasMeaningfulMessage when empty and then with ignored types, always return false`() {
assertThat(messageTable.hasMeaningfulMessage(1)).isFalse()
assertThat(SignalDatabase.messages.hasMeaningfulMessage(1)).isFalse()
TestSms.insert(db, type = MessageTypes.IGNORABLE_TYPESMASK_WHEN_COUNTING)
assertThat(messageTable.hasMeaningfulMessage(1)).isFalse()
TestSms.insert(signalDatabaseRule.writeableDatabase, type = MessageTypes.IGNORABLE_TYPESMASK_WHEN_COUNTING)
assertThat(SignalDatabase.messages.hasMeaningfulMessage(1)).isFalse()
TestSms.insert(db, type = MessageTypes.PROFILE_CHANGE_TYPE)
assertThat(messageTable.hasMeaningfulMessage(1)).isFalse()
TestSms.insert(signalDatabaseRule.writeableDatabase, type = MessageTypes.PROFILE_CHANGE_TYPE)
assertThat(SignalDatabase.messages.hasMeaningfulMessage(1)).isFalse()
TestSms.insert(db, type = MessageTypes.CHANGE_NUMBER_TYPE)
assertThat(messageTable.hasMeaningfulMessage(1)).isFalse()
TestSms.insert(signalDatabaseRule.writeableDatabase, type = MessageTypes.CHANGE_NUMBER_TYPE)
assertThat(SignalDatabase.messages.hasMeaningfulMessage(1)).isFalse()
TestSms.insert(db, type = MessageTypes.RELEASE_CHANNEL_DONATION_REQUEST_TYPE)
assertThat(messageTable.hasMeaningfulMessage(1)).isFalse()
TestSms.insert(signalDatabaseRule.writeableDatabase, type = MessageTypes.RELEASE_CHANNEL_DONATION_REQUEST_TYPE)
assertThat(SignalDatabase.messages.hasMeaningfulMessage(1)).isFalse()
TestSms.insert(db, type = MessageTypes.SMS_EXPORT_TYPE)
assertThat(messageTable.hasMeaningfulMessage(1)).isFalse()
TestSms.insert(signalDatabaseRule.writeableDatabase, type = MessageTypes.SMS_EXPORT_TYPE)
assertThat(SignalDatabase.messages.hasMeaningfulMessage(1)).isFalse()
TestSms.insert(db, type = MessageTypes.BASE_INBOX_TYPE or MessageTypes.GROUP_V2_LEAVE_BITS)
assertThat(messageTable.hasMeaningfulMessage(1)).isFalse()
TestSms.insert(signalDatabaseRule.writeableDatabase, type = MessageTypes.BASE_INBOX_TYPE or MessageTypes.GROUP_V2_LEAVE_BITS)
assertThat(SignalDatabase.messages.hasMeaningfulMessage(1)).isFalse()
}
}

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.recipients.Recipient
@@ -12,12 +13,14 @@ import org.thoughtcrime.securesms.recipients.RecipientId
*/
object TestMms {
private var startSendTimestamp = System.currentTimeMillis()
fun insert(
db: SQLiteDatabase,
db: SupportSQLiteDatabase,
recipient: Recipient = Recipient.UNKNOWN,
recipientId: RecipientId = Recipient.UNKNOWN.id,
body: String = "body",
sentTimeMillis: Long = System.currentTimeMillis(),
sentTimeMillis: Long = startSendTimestamp++,
receivedTimestampMillis: Long = System.currentTimeMillis(),
expiresIn: Long = 0,
expireTimerVersion: Int = 1,
@@ -64,7 +67,7 @@ object TestMms {
}
private fun insert(
db: SQLiteDatabase,
db: SupportSQLiteDatabase,
message: OutgoingMessage,
recipientId: RecipientId = message.threadRecipient.id,
body: String = message.body,
@@ -96,7 +99,7 @@ object TestMms {
put(MessageTable.MENTIONS_SELF, 0)
}
return db.insert(MessageTable.TABLE_NAME, null, contentValues)
return db.insert(MessageTable.TABLE_NAME, 0, contentValues)
}
fun markAsRemoteDelete(db: SQLiteDatabase, messageId: Long) {

View File

@@ -1,24 +1,26 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import androidx.sqlite.db.SupportSQLiteDatabase
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.recipients.RecipientId
import java.util.Optional
import java.util.UUID
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
/**
* Helper methods for inserting SMS messages into the SMS table.
*/
object TestSms {
private var startSentTimestamp = System.currentTimeMillis()
fun insert(
db: AndroidSQLiteDatabase,
db: SupportSQLiteDatabase,
sender: RecipientId = RecipientId.from(1),
senderDeviceId: Int = 1,
sentTimestampMillis: Long = System.currentTimeMillis(),
sentTimestampMillis: Long = startSentTimestamp++,
serverTimestampMillis: Long = System.currentTimeMillis(),
receivedTimestampMillis: Long = System.currentTimeMillis(),
encodedBody: String = "encodedBody",
@@ -53,7 +55,7 @@ object TestSms {
}
fun insert(
db: AndroidSQLiteDatabase,
db: SupportSQLiteDatabase,
message: IncomingMessage,
type: Long = MessageTypes.BASE_INBOX_TYPE,
unread: Boolean = false,
@@ -75,6 +77,6 @@ object TestSms {
put(MessageTable.SERVER_GUID, message.serverGuid)
}
return db.insert(MessageTable.TABLE_NAME, null, values)
return db.insert(MessageTable.TABLE_NAME, 0, values)
}
}

View File

@@ -1,30 +0,0 @@
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

@@ -1,25 +0,0 @@
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
}
}

View File

@@ -1,26 +1,28 @@
package org.thoughtcrime.securesms.testing
import android.app.Application
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.DatabaseSecret
import org.thoughtcrime.securesms.database.SignalDatabase
import java.security.SecureRandom
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
import net.zetetic.database.sqlcipher.SQLiteDatabase as SQLCipherSQLiteDatabase
/**
* Proxy [SignalDatabase] to the [TestSQLiteOpenHelper] interface.
* Test flavor of [SignalDatabase].
*/
class ProxySQLCipherOpenHelper(
class TestSignalDatabase(
context: Application,
val myReadableDatabase: AndroidSQLiteDatabase,
val myWritableDatabase: AndroidSQLiteDatabase
val supportReadableDatabase: SupportSQLiteDatabase,
val supportWritableDatabase: SupportSQLiteDatabase
) : SignalDatabase(context, DatabaseSecret(ByteArray(32).apply { SecureRandom().nextBytes(this) }), AttachmentSecret()) {
constructor(context: Application, testOpenHelper: TestSQLiteOpenHelper) : this(context, testOpenHelper.readableDatabase, testOpenHelper.writableDatabase)
constructor(context: Application, testOpenHelper: SupportSQLiteOpenHelper) : this(context, testOpenHelper.readableDatabase, testOpenHelper.writableDatabase)
override fun close() {
throw UnsupportedOperationException()
supportReadableDatabase.close()
supportWritableDatabase.close()
}
override val databaseName: String
@@ -66,11 +68,13 @@ class ProxySQLCipherOpenHelper(
override val rawWritableDatabase: net.zetetic.database.sqlcipher.SQLiteDatabase
get() = throw UnsupportedOperationException()
override val signalReadableDatabase: org.thoughtcrime.securesms.database.SQLiteDatabase
get() = ProxySignalSQLiteDatabase(myReadableDatabase)
override val signalReadableDatabase: org.thoughtcrime.securesms.database.SQLiteDatabase by lazy {
TestSignalSQLiteDatabase(supportReadableDatabase)
}
override val signalWritableDatabase: org.thoughtcrime.securesms.database.SQLiteDatabase
get() = ProxySignalSQLiteDatabase(myWritableDatabase)
override val signalWritableDatabase: org.thoughtcrime.securesms.database.SQLiteDatabase by lazy {
TestSignalSQLiteDatabase(supportWritableDatabase)
}
override fun getSqlCipherDatabase(): SQLCipherSQLiteDatabase {
throw UnsupportedOperationException()

View File

@@ -2,8 +2,10 @@ package org.thoughtcrime.securesms.testing
import android.content.ContentValues
import android.database.Cursor
import android.database.SQLException
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteQuery
import org.signal.core.util.toAndroidQuery
import net.zetetic.database.sqlcipher.SQLiteQueryBuilder
import java.util.Locale
import android.database.sqlite.SQLiteDatabase as AndroidSQLiteDatabase
import android.database.sqlite.SQLiteTransactionListener as AndroidSQLiteTransactionListener
@@ -15,7 +17,7 @@ import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSQLiteDatabas
* Partial implementation of [SignalSQLiteDatabase] using an instance of [AndroidSQLiteDatabase] instead
* of SQLCipher.
*/
class ProxySignalSQLiteDatabase(private val database: AndroidSQLiteDatabase) : SignalSQLiteDatabase(null) {
class TestSignalSQLiteDatabase(private val database: SupportSQLiteDatabase) : SignalSQLiteDatabase(null) {
override fun getSqlCipherDatabase(): net.zetetic.database.sqlcipher.SQLiteDatabase {
throw UnsupportedOperationException()
}
@@ -33,7 +35,7 @@ class ProxySignalSQLiteDatabase(private val database: AndroidSQLiteDatabase) : S
}
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)
throw UnsupportedOperationException()
}
override fun queryWithFactory(
@@ -48,75 +50,83 @@ class ProxySignalSQLiteDatabase(private val database: AndroidSQLiteDatabase) : S
orderBy: String?,
limit: String?
): Cursor {
return database.queryWithFactory(null, distinct, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit)
throw UnsupportedOperationException()
}
override fun query(query: SupportSQLiteQuery): Cursor {
val converted = query.toAndroidQuery()
return database.rawQuery(converted.where, converted.whereArgs)
}
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)
return database.query(query)
}
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)
val query: String = SQLiteQueryBuilder.buildQueryString(false, table, columns, selection, groupBy, having, orderBy, limit)
return database.query(query, selectionArgs ?: emptyArray())
}
override fun query(table: String, columns: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, groupBy: String?, having: String?, orderBy: String?): Cursor {
return query(table, columns, selection, selectionArgs, groupBy, having, orderBy, null)
}
override fun rawQuery(sql: String, selectionArgs: Array<out String>?): Cursor {
return database.rawQuery(sql, selectionArgs)
return database.query(sql, selectionArgs ?: emptyArray())
}
override fun rawQuery(sql: String, args: Array<out Any>?): Cursor {
return database.rawQuery(sql, args?.map(Any::toString)?.toTypedArray())
return database.query(sql, args ?: emptyArray())
}
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)
throw UnsupportedOperationException()
}
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 insert(table: String, nullColumnHack: String?, values: ContentValues): Long {
return database.insert(table, 0, values)
}
override fun insertOrThrow(table: String, nullColumnHack: String?, values: ContentValues?): Long {
return database.insertOrThrow(table, nullColumnHack, values)
override fun insertOrThrow(table: String, nullColumnHack: String?, values: ContentValues): Long {
val result = database.insert(table, 0, values)
if (result < 0) {
throw SQLException()
}
return result
}
override fun replace(table: String, nullColumnHack: String?, initialValues: ContentValues?): Long {
return database.replace(table, nullColumnHack, initialValues)
override fun replace(table: String, nullColumnHack: String?, initialValues: ContentValues): Long {
return database.insert(table, 5, initialValues)
}
override fun replaceOrThrow(table: String, nullColumnHack: String?, initialValues: ContentValues?): Long {
return database.replaceOrThrow(table, nullColumnHack, initialValues)
override fun replaceOrThrow(table: String, nullColumnHack: String?, initialValues: ContentValues): Long {
val result = replace(table, nullColumnHack, initialValues)
if (result < 0) {
throw SQLException()
}
return result
}
override fun insertWithOnConflict(table: String, nullColumnHack: String?, initialValues: ContentValues?, conflictAlgorithm: Int): Long {
return database.insertWithOnConflict(table, nullColumnHack, initialValues, conflictAlgorithm)
override fun insertWithOnConflict(table: String, nullColumnHack: String?, initialValues: ContentValues, conflictAlgorithm: Int): Long {
return database.insert(table, conflictAlgorithm, initialValues)
}
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 update(table: String, values: ContentValues, whereClause: String?, whereArgs: Array<out String>?): Int {
return database.update(table, 0, 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 updateWithOnConflict(table: String, values: ContentValues, whereClause: String?, whereArgs: Array<out String>?, conflictAlgorithm: Int): Int {
return database.update(table, conflictAlgorithm, values, whereClause, whereArgs)
}
override fun execSQL(sql: String) {
database.execSQL(sql)
}
override fun rawExecSQL(sql: String?) {
override fun rawExecSQL(sql: String) {
database.execSQL(sql)
}
@@ -135,8 +145,8 @@ class ProxySignalSQLiteDatabase(private val database: AndroidSQLiteDatabase) : S
override val isWriteAheadLoggingEnabled: Boolean
get() = throw UnsupportedOperationException()
override fun setForeignKeyConstraintsEnabled(enable: Boolean) {
database.setForeignKeyConstraintsEnabled(enable)
override fun setForeignKeyConstraintsEnabled(enabled: Boolean) {
database.setForeignKeyConstraintsEnabled(enabled)
}
override fun beginTransactionWithListener(transactionListener: SQLCipherSQLiteTransactionListener?) {
@@ -182,23 +192,22 @@ class ProxySignalSQLiteDatabase(private val database: AndroidSQLiteDatabase) : S
override val isDbLockedByCurrentThread: Boolean
get() = database.isDbLockedByCurrentThread
@Suppress("DEPRECATION")
override fun isDbLockedByOtherThreads(): Boolean {
return database.isDbLockedByOtherThreads
return false
}
override fun yieldIfContendedSafely(): Boolean {
return database.yieldIfContendedSafely()
}
override fun yieldIfContendedSafely(sleepAfterYieldDelay: Long): Boolean {
return database.yieldIfContendedSafely(sleepAfterYieldDelay)
override fun yieldIfContendedSafely(sleepAfterYieldDelayMillis: Long): Boolean {
return database.yieldIfContendedSafely(sleepAfterYieldDelayMillis)
}
override var version: Int
get() = database.version
set(value) {
database.version = version
database.version = value
}
override val maximumSize: Long

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.testutil
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.unmockkObject
import org.junit.rules.ExternalResource
import org.thoughtcrime.securesms.database.SQLiteDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.testing.TestSignalDatabase
class SignalDatabaseRule : ExternalResource() {
lateinit var signalDatabase: TestSignalDatabase
val readableDatabase: SQLiteDatabase
get() = signalDatabase.signalReadableDatabase
val writeableDatabase: SQLiteDatabase
get() = signalDatabase.signalWritableDatabase
override fun before() {
signalDatabase = inMemorySignalDatabase()
mockkObject(SignalDatabase)
every { SignalDatabase.instance } returns signalDatabase
}
override fun after() {
unmockkObject(SignalDatabase)
signalDatabase.close()
}
companion object {
/**
* Create an in-memory only database mimicking one created fresh for Signal. This includes
* all non-FTS tables, indexes, and triggers.
*/
private fun inMemorySignalDatabase(): TestSignalDatabase {
val configuration = SupportSQLiteOpenHelper.Configuration(
context = ApplicationProvider.getApplicationContext(),
name = "test",
callback = object : SupportSQLiteOpenHelper.Callback(1) {
override fun onCreate(db: SupportSQLiteDatabase) = Unit
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) = Unit
},
useNoBackupDirectory = false,
allowDataLossOnRecovery = true
)
val helper = FrameworkSQLiteOpenHelperFactory().create(configuration)
val signalDatabase = TestSignalDatabase(ApplicationProvider.getApplicationContext(), helper)
signalDatabase.onCreateTablesIndexesAndTriggers(signalDatabase.signalWritableDatabase)
return signalDatabase
}
}
}