fix(data): account-lifecycle data integrity (non-destructive upsert, id normalization, deleteAccount cleanup)
#309: AccountDao no longer uses @Insert(REPLACE). New insertIfAbsent (IGNORE) + @Update back a non-destructive upsert, and insertAtEnd updates an existing id in place (preserving its sortOrder) instead of REPLACE. Re-adding an existing account id (e.g. re-authing an Outlook account, whose id is the deterministic outlook:<email>) therefore no longer cascade-deletes its account_settings + signatures. #305: normalizeEmailForAccountId always lowercases the domain (mail domains are case-insensitive), and the whole address for the consumer providers (Gmail, Yahoo, iCloud, AOL, Outlook). Applied at every id-derivation site (MailProvider.createAccount, Account.outlook, ManualSetupViewModel) so differently-cased addresses can't spawn duplicate accounts. The displayed email keeps the user's casing. #299: deleteAccount collects the account's message ids and draft attachment URIs while the rows still exist, deletes the rows (now including the account's drafts), then deleteRecursively()'s each message's on-disk attachment cache dir and releases the drafts' now-unreferenced persistable URI grants. Adds unit tests for id normalization + deleteAccount cleanup and DAO-level instrumented tests for the non-destructive create/update. Closes #309 Closes #305 Closes #299 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -67,7 +67,9 @@ class AccountDaoTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun upsertReplacesAnAccountWithTheSameId() = runBlocking {
|
||||
fun upsertOnAConflictingIdUpdatesTheRowInPlace() = runBlocking {
|
||||
// Non-destructive by design (issue #309): a second upsert of the same id refreshes the row
|
||||
// rather than delete-then-reinserting it (which would cascade-delete settings/signatures).
|
||||
dao.upsert(account("acct", "ada@example.org", displayName = "Ada"))
|
||||
|
||||
dao.upsert(account("acct", "ada@example.org", displayName = "Ada Lovelace"))
|
||||
|
||||
@@ -110,4 +110,40 @@ class AccountDatabaseTest {
|
||||
db.signatureDao().observeForAccount("acct").first().isEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun upsertOnAnExistingIdUpdatesInPlaceWithoutCascadingSettingsOrSignatures() = runBlocking<Unit> {
|
||||
// Regression for issue #309: an @Insert(REPLACE) upsert deletes-then-reinserts on an id
|
||||
// conflict, firing the ON DELETE CASCADE that wipes the account's settings + signatures.
|
||||
db.accountDao().upsert(account("acct"))
|
||||
db.accountSettingsDao().upsert(AccountSettingsEntity("acct", signature = "Keep me"))
|
||||
db.signatureDao().upsert(SignatureEntity("sig-1", "acct", "Work", "<p>Regards</p>", isDefault = true))
|
||||
|
||||
db.accountDao().upsert(account("acct").copy(displayName = "Updated"))
|
||||
|
||||
assertEquals("Updated", db.accountDao().getById("acct")?.displayName)
|
||||
assertEquals("Keep me", db.accountSettingsDao().get("acct")?.signature)
|
||||
assertEquals(1, db.signatureDao().observeForAccount("acct").first().size)
|
||||
assertEquals(1, db.accountDao().getAll().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertAtEndReAddingAnExistingAccountKeepsItsSettingsSignaturesAndPosition() = runBlocking<Unit> {
|
||||
// The production re-add path (addOutlookAccount -> insertAtEnd -> upsert). Re-adding a
|
||||
// deterministic id must not cascade-delete its settings/signatures nor move it to the end.
|
||||
db.accountDao().insertAtEnd(account("a"))
|
||||
db.accountDao().insertAtEnd(account("b"))
|
||||
db.accountSettingsDao().upsert(AccountSettingsEntity("b", signature = "Sig B", signatureEnabled = false))
|
||||
db.signatureDao().upsert(SignatureEntity("sig-b", "b", "Work", "<p>Regards</p>", isDefault = true))
|
||||
|
||||
db.accountDao().insertAtEnd(account("b").copy(displayName = "Renamed"))
|
||||
|
||||
// Updated in place, position preserved (still 1, not appended after), children intact.
|
||||
assertEquals("Renamed", db.accountDao().getById("b")?.displayName)
|
||||
assertEquals(1, db.accountDao().getById("b")?.sortOrder)
|
||||
assertEquals("Sig B", db.accountSettingsDao().get("b")?.signature)
|
||||
assertEquals(false, db.accountSettingsDao().get("b")?.signatureEnabled)
|
||||
assertEquals(1, db.signatureDao().observeForAccount("b").first().size)
|
||||
assertEquals(2, db.accountDao().getAll().size)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user