Merge main into fix-219-empty-state-flash
This commit is contained in:
@@ -221,11 +221,38 @@ class AccountDataMigratorTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copiedAccountsGetAStableAlphabeticalInitialSortOrder() = runBlocking<Unit> {
|
||||
// Insert three accounts in non-alphabetical order so the assertion proves the initial order is
|
||||
// by email (the pre-#111 listing) rather than the copy's insertion order (issue #164). sortOrder
|
||||
// is a destination-only column the v14 cache never had, so it is assigned entirely by the copy.
|
||||
helper.createDatabase(cacheName, 14).apply {
|
||||
listOf("c" to "carol@example.org", "a" to "ada@example.org", "b" to "bob@example.org")
|
||||
.forEach { (id, email) ->
|
||||
execSQL(
|
||||
"INSERT INTO accounts (id, email, displayName, authType, imap_host, imap_port, " +
|
||||
"imap_security, smtp_host, smtp_port, smtp_security) VALUES " +
|
||||
"('$id', '$email', '$id', 'PASSWORD_IMAP', 'imap.example.org', 993, 'SSL_TLS', " +
|
||||
"'smtp.example.org', 465, 'SSL_TLS')",
|
||||
)
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
AccountDataMigrator.copyAccountTables(cacheFile, cachePassphrase = "", accountsFile = accountsFile)
|
||||
|
||||
openAccountsDb().apply {
|
||||
// getAll orders by sortOrder; the copy ranked by email, so ada(0) < bob(1) < carol(2).
|
||||
assertEquals(listOf("a", "b", "c"), accountDao().getAll().map { it.id })
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migratorDdlMatchesExportedAccountDatabaseSchema() {
|
||||
val schema = JSONObject(
|
||||
InstrumentationRegistry.getInstrumentation().context.assets
|
||||
.open("org.libremail.data.local.AccountDatabase/1.json")
|
||||
.open("org.libremail.data.local.AccountDatabase/2.json")
|
||||
.bufferedReader().use { it.readText() },
|
||||
).getJSONObject("database")
|
||||
val entities = schema.getJSONArray("entities")
|
||||
|
||||
@@ -39,9 +39,9 @@ class AccountDatabaseTest {
|
||||
@After
|
||||
fun tearDown() = db.close()
|
||||
|
||||
private fun account(id: String = "acct") = AccountEntity(
|
||||
private fun account(id: String = "acct", email: String = "$id@example.org") = AccountEntity(
|
||||
id = id,
|
||||
email = "a@example.org",
|
||||
email = email,
|
||||
displayName = "A",
|
||||
authType = "PASSWORD_IMAP",
|
||||
imap = ServerConfigEmbedded("imap.example.org", 993, "SSL_TLS"),
|
||||
@@ -69,6 +69,34 @@ class AccountDatabaseTest {
|
||||
assertNull("account_settings must cascade-delete with its account", db.accountSettingsDao().get("acct"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertAtEndAppendsAccountsInIncreasingSortOrder() = runBlocking<Unit> {
|
||||
db.accountDao().insertAtEnd(account("a"))
|
||||
db.accountDao().insertAtEnd(account("b"))
|
||||
db.accountDao().insertAtEnd(account("c"))
|
||||
|
||||
// Each new account lands at the end: current max + 1, starting from 0 (issue #164).
|
||||
assertEquals(0, db.accountDao().getById("a")?.sortOrder)
|
||||
assertEquals(1, db.accountDao().getById("b")?.sortOrder)
|
||||
assertEquals(2, db.accountDao().getById("c")?.sortOrder)
|
||||
// observeAll now orders by sortOrder, i.e. the append (insertion) order.
|
||||
assertEquals(listOf("a", "b", "c"), db.accountDao().observeAll().first().map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reorderPersistsAndBothQueriesReflectTheNewOrder() = runBlocking<Unit> {
|
||||
listOf("a", "b", "c").forEach { db.accountDao().insertAtEnd(account(it)) }
|
||||
|
||||
db.accountDao().reorder(listOf("c", "a", "b"))
|
||||
|
||||
assertEquals(listOf("c", "a", "b"), db.accountDao().observeAll().first().map { it.id })
|
||||
assertEquals(listOf("c", "a", "b"), db.accountDao().getAll().map { it.id })
|
||||
// sortOrder is renumbered to the new positions, so the order survives an app restart.
|
||||
assertEquals(0, db.accountDao().getById("c")?.sortOrder)
|
||||
assertEquals(1, db.accountDao().getById("a")?.sortOrder)
|
||||
assertEquals(2, db.accountDao().getById("b")?.sortOrder)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signaturesCascadeWithTheirAccount() = runBlocking<Unit> {
|
||||
db.accountDao().upsert(account())
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package org.libremail.data.local
|
||||
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Replays [AccountDatabase]'s migrations against the schema JSONs exported to `app/schemas` (shipped
|
||||
* to the test APK as assets). AccountDatabase is the non-auth account store split out of the cache in
|
||||
* issue #111; like the cache database it registers no destructive fallback, so a drifted migration
|
||||
* would crash upgrading users at first open — this test makes such drift fail in CI instead. Kept
|
||||
* separate from [MigrationTest] because each database has its own version line and MigrationTestHelper
|
||||
* binds to a single [androidx.room.RoomDatabase] class.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AccountMigrationTest {
|
||||
|
||||
@get:Rule
|
||||
val helper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AccountDatabase::class.java,
|
||||
emptyList(),
|
||||
FrameworkSQLiteOpenHelperFactory(),
|
||||
)
|
||||
|
||||
/**
|
||||
* v1 -> v2 (issue #164): `accounts.sortOrder` appears, and existing accounts are backfilled with a
|
||||
* stable initial order matching the previous alphabetical (`ORDER BY email`) listing. The rows are
|
||||
* inserted out of alphabetical order to prove the rank is by email, not insertion order.
|
||||
*/
|
||||
@Test
|
||||
fun migrate1To2_addsSortOrderBackfilledByEmailRank() {
|
||||
helper.createDatabase(TEST_DB, 1).apply {
|
||||
insertAccount("c", "carol@example.org")
|
||||
insertAccount("a", "ada@example.org")
|
||||
insertAccount("b", "bob@example.org")
|
||||
close()
|
||||
}
|
||||
|
||||
val db = helper.runMigrationsAndValidate(TEST_DB, 2, true, ACCOUNT_MIGRATION_1_2)
|
||||
|
||||
db.query("SELECT id, sortOrder FROM accounts ORDER BY sortOrder").use { c ->
|
||||
assertTrue(c.moveToFirst())
|
||||
assertEquals("ada ranks first alphabetically, so sortOrder 0", "a", c.getString(0))
|
||||
assertEquals(0, c.getInt(1))
|
||||
assertTrue(c.moveToNext())
|
||||
assertEquals("b", c.getString(0))
|
||||
assertEquals(1, c.getInt(1))
|
||||
assertTrue(c.moveToNext())
|
||||
assertEquals("c", c.getString(0))
|
||||
assertEquals(2, c.getInt(1))
|
||||
assertFalse("all three pre-upgrade accounts must survive, and nothing else", c.moveToNext())
|
||||
}
|
||||
db.close()
|
||||
}
|
||||
|
||||
private fun SupportSQLiteDatabase.insertAccount(id: String, email: String) {
|
||||
execSQL(
|
||||
"INSERT INTO accounts (id, email, displayName, authType, imap_host, imap_port, imap_security, " +
|
||||
"smtp_host, smtp_port, smtp_security) VALUES ('$id', '$email', '$id', 'PASSWORD_IMAP', " +
|
||||
"'imap.example.org', 993, 'SSL_TLS', 'smtp.example.org', 465, 'SSL_TLS')",
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TEST_DB = "account-migration-test.db"
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,11 @@ class FakeAccountRepository(
|
||||
accountsFlow.value = accountsFlow.value.filterNot { it.id == id }
|
||||
}
|
||||
|
||||
override suspend fun reorderAccounts(orderedIds: List<String>) {
|
||||
val byId = accountsFlow.value.associateBy { it.id }
|
||||
accountsFlow.value = orderedIds.mapNotNull { byId[it] }
|
||||
}
|
||||
|
||||
override suspend fun resetBackfillProgress(accountId: String?) = Unit
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user