package com.fsck.k9.mailstore; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import android.app.Application; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.text.TextUtils; import com.fsck.k9.Account; import com.fsck.k9.BuildConfig; import com.fsck.k9.GlobalsHelper; import com.fsck.k9.K9; import com.fsck.k9.K9RobolectricTestRunner; import com.fsck.k9.R; import com.fsck.k9.mail.MessagingException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLog; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @RunWith(K9RobolectricTestRunner.class) @Config(manifest = Config.NONE) public class StoreSchemaDefinitionTest { private StoreSchemaDefinition storeSchemaDefinition; @Before public void setUp() throws MessagingException { ShadowLog.stream = System.out; Application application = RuntimeEnvironment.application; K9.app = application; GlobalsHelper.setContext(application); StorageManager.getInstance(application); storeSchemaDefinition = createStoreSchemaDefinition(); } @Test public void getVersion_shouldReturnCurrentDatabaseVersion() { int version = storeSchemaDefinition.getVersion(); assertEquals(LocalStore.DB_VERSION, version); } @Test public void doDbUpgrade_withEmptyDatabase_shouldSetsDatabaseVersion() { SQLiteDatabase database = SQLiteDatabase.create(null); storeSchemaDefinition.doDbUpgrade(database); assertEquals(LocalStore.DB_VERSION, database.getVersion()); } @Test public void doDbUpgrade_withBadDatabase_shouldThrowInDebugBuild() { if (BuildConfig.DEBUG) { SQLiteDatabase database = SQLiteDatabase.create(null); database.setVersion(29); try { storeSchemaDefinition.doDbUpgrade(database); fail("Expected Error"); } catch (Error e) { assertEquals("Exception while upgrading database", e.getMessage()); } } } @Test public void doDbUpgrade_withV29_shouldUpgradeDatabaseToLatestVersion() { SQLiteDatabase database = createV29Database(); storeSchemaDefinition.doDbUpgrade(database); assertEquals(LocalStore.DB_VERSION, database.getVersion()); } @Test public void doDbUpgrade_withV29() { SQLiteDatabase database = createV29Database(); insertMessageWithSubject(database, "Test Email"); storeSchemaDefinition.doDbUpgrade(database); assertMessageWithSubjectExists(database, "Test Email"); } @Test public void doDbUpgrade_fromV29_shouldResultInSameTables() { SQLiteDatabase newDatabase = createNewDatabase(); SQLiteDatabase upgradedDatabase = createV29Database(); storeSchemaDefinition.doDbUpgrade(upgradedDatabase); assertDatabaseTablesEquals(newDatabase, upgradedDatabase); } @Test public void doDbUpgrade_fromV29_shouldResultInSameTriggers() { SQLiteDatabase newDatabase = createNewDatabase(); SQLiteDatabase upgradedDatabase = createV29Database(); storeSchemaDefinition.doDbUpgrade(upgradedDatabase); assertDatabaseTriggersEquals(newDatabase, upgradedDatabase); } @Test public void doDbUpgrade_fromV29_shouldResultInSameIndexes() { SQLiteDatabase newDatabase = createNewDatabase(); SQLiteDatabase upgradedDatabase = createV29Database(); storeSchemaDefinition.doDbUpgrade(upgradedDatabase); assertDatabaseIndexesEquals(newDatabase, upgradedDatabase); } private SQLiteDatabase createV29Database() { SQLiteDatabase database = SQLiteDatabase.create(null); initV29Database(database); return database; } private void initV29Database(SQLiteDatabase db) { /* * There is no precise definition of a v29 database. This function approximates it by creating a database * that could be upgraded to the latest database as of v58 */ db.beginTransaction(); db.execSQL("CREATE TABLE folders (" + "id INTEGER PRIMARY KEY," + "name TEXT, " + "last_updated INTEGER, " + "unread_count INTEGER, " + "visible_limit INTEGER, " + "status TEXT, " + "push_state TEXT, " + "last_pushed INTEGER " + ")"); db.execSQL("CREATE TABLE messages (" + "id INTEGER PRIMARY KEY, " + "folder_id INTEGER, " + "uid TEXT, " + "subject TEXT, " + "date INTEGER, " + "flags TEXT, " + "sender_list TEXT, " + "to_list TEXT, " + "cc_list TEXT, " + "bcc_list TEXT, " + "reply_to_list TEXT, " + "attachment_count INTEGER, " + "internal_date INTEGER, " + "message_id TEXT, " + "html_content TEXT, " + "text_content TEXT, " + "preview_type TEXT default \"none\", " + "message_part_id INTEGER" + ")"); db.execSQL("CREATE TABLE attachments (" + "id INTEGER PRIMARY KEY, " + "size INTEGER, " + "name TEXT, " + "mime_type TEXT, " + "store_data TEXT, " + "content_uri TEXT, " + "message_id INTEGER" + ")"); db.execSQL("CREATE TABLE headers (" + "id INTEGER PRIMARY KEY, " + "name TEXT, " + "value TEXT, " + "message_id INTEGER" + ")"); db.execSQL("CREATE TABLE threads (" + "id INTEGER PRIMARY KEY, " + "message_id INTEGER, " + "root INTEGER, " + "parent INTEGER" + ")"); db.execSQL("CREATE TABLE pending_commands (" + "id INTEGER PRIMARY KEY, " + "command TEXT, " + "arguments TEXT" + ")"); db.execSQL("CREATE INDEX msg_uid ON messages (uid, folder_id)"); db.execSQL("CREATE INDEX folder_name ON folders (name)"); db.execSQL("CREATE INDEX threads_message_id ON threads (message_id)"); db.execSQL("CREATE INDEX threads_root ON threads (root)"); db.execSQL("CREATE INDEX threads_parent ON threads (parent)"); db.execSQL("CREATE TRIGGER set_thread_root " + "AFTER INSERT ON threads " + "BEGIN " + "UPDATE threads SET root=id WHERE root IS NULL AND ROWID = NEW.ROWID; " + "END"); db.execSQL("CREATE TRIGGER delete_folder " + "BEFORE DELETE ON folders " + "BEGIN " + "DELETE FROM messages WHERE old.id = folder_id; " + "END;"); db.execSQL("CREATE TRIGGER delete_message " + "BEFORE DELETE ON messages " + "BEGIN " + "DELETE FROM message_parts WHERE root = OLD.message_part_id; " + "DELETE FROM messages_fulltext WHERE docid = OLD.id; " + "END"); db.setVersion(29); db.setTransactionSuccessful(); db.endTransaction(); } private void assertMessageWithSubjectExists(SQLiteDatabase database, String subject) { Cursor cursor = database.query("messages", new String[] { "subject" }, null, null, null, null, null); try { assertTrue(cursor.moveToFirst()); assertEquals(subject, cursor.getString(0)); } finally { cursor.close(); } } private void assertDatabaseTablesEquals(SQLiteDatabase expected, SQLiteDatabase actual) { List<String> tablesInNewDatabase = tablesInDatabase(expected); Collections.sort(tablesInNewDatabase); List<String> tablesInUpgradedDatabase = tablesInDatabase(actual); Collections.sort(tablesInUpgradedDatabase); assertEquals(tablesInNewDatabase, tablesInUpgradedDatabase); } private void assertDatabaseTriggersEquals(SQLiteDatabase expected, SQLiteDatabase actual) { List<String> triggersInNewDatabase = triggersInDatabase(expected); Collections.sort(triggersInNewDatabase); List<String> triggersInUpgradedDatabase = triggersInDatabase(actual); Collections.sort(triggersInUpgradedDatabase); assertEquals(triggersInNewDatabase, triggersInUpgradedDatabase); } private void assertDatabaseIndexesEquals(SQLiteDatabase expected, SQLiteDatabase actual) { List<String> indexesInNewDatabase = indexesInDatabase(expected); Collections.sort(indexesInNewDatabase); List<String> indexesInUpgradedDatabase = indexesInDatabase(actual); Collections.sort(indexesInUpgradedDatabase); assertEquals(indexesInNewDatabase, indexesInUpgradedDatabase); } private List<String> tablesInDatabase(SQLiteDatabase db) { return objectsInDatabase(db, "table"); } private List<String> triggersInDatabase(SQLiteDatabase db) { return objectsInDatabase(db, "trigger"); } private List<String> indexesInDatabase(SQLiteDatabase db) { return objectsInDatabase(db, "index"); } private List<String> objectsInDatabase(SQLiteDatabase db, String type) { List<String> databaseObjects = new ArrayList<>(); Cursor cursor = db.rawQuery("SELECT sql FROM sqlite_master WHERE type = ? AND sql IS NOT NULL", new String[] { type }); try { while (cursor.moveToNext()) { String sql = cursor.getString(cursor.getColumnIndex("sql")); String resortedSql = "table".equals(type) ? sortTableColumns(sql) : sql; databaseObjects.add(resortedSql); } } finally { cursor.close(); } return databaseObjects; } private String sortTableColumns(String sql) { int positionOfColumnDefinitions = sql.indexOf('('); String columnDefinitionsSql = sql.substring(positionOfColumnDefinitions + 1, sql.length() - 1); String[] columnDefinitions = columnDefinitionsSql.split(" *, *(?![^(]*\\))"); Arrays.sort(columnDefinitions); String sqlPrefix = sql.substring(0, positionOfColumnDefinitions + 1); String sortedColumnDefinitionsSql = TextUtils.join(", ", columnDefinitions); return sqlPrefix + sortedColumnDefinitionsSql + ")"; } private void insertMessageWithSubject(SQLiteDatabase database, String subject) { ContentValues data = new ContentValues(); data.put("subject", subject); long rowId = database.insert("messages", null, data); assertNotEquals(-1, rowId); } private StoreSchemaDefinition createStoreSchemaDefinition() throws MessagingException { Context context = createContext(); Account account = createAccount(); LockableDatabase lockableDatabase = createLockableDatabase(); LocalStore localStore = mock(LocalStore.class); localStore.database = lockableDatabase; when(localStore.getContext()).thenReturn(context); when(localStore.getAccount()).thenReturn(account); return new StoreSchemaDefinition(localStore); } private Context createContext() { Context context = mock(Context.class); when(context.getString(R.string.special_mailbox_name_outbox)).thenReturn("Outbox"); return context; } private LockableDatabase createLockableDatabase() throws MessagingException { LockableDatabase lockableDatabase = mock(LockableDatabase.class); when(lockableDatabase.execute(anyBoolean(), any(LockableDatabase.DbCallback.class))).thenReturn(false); return lockableDatabase; } private Account createAccount() { Account account = mock(Account.class); when(account.getInboxFolderName()).thenReturn("Inbox"); when(account.getLocalStorageProviderId()).thenReturn(StorageManager.InternalStorageProvider.ID); return account; } private SQLiteDatabase createNewDatabase() { SQLiteDatabase database = SQLiteDatabase.create(null); storeSchemaDefinition.doDbUpgrade(database); return database; } }