package com.b2msolutions.reyna; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import com.b2msolutions.reyna.system.Header; import com.b2msolutions.reyna.system.Message; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import java.net.URI; import java.net.URISyntaxException; import static org.junit.Assert.*; import static org.mockito.Mockito.*; @Config(sdk = 18) @RunWith(RobolectricTestRunner.class) public class RepositoryTest { private Repository repository; @Before public void setup() { Context context = RuntimeEnvironment.application.getApplicationContext(); this.repository = new Repository(context); } @Test public void constructionShouldNotThrow() { assertNotNull(this.repository); } @Test public void onCreateShouldNotThrow() { SQLiteDatabase db = this.repository.getReadableDatabase(); assertNotNull(db); } @Test public void insertWithNullMessageShouldNotThrow() { this.repository.insert(null); } @Test public void insertShouldNotThrow() throws URISyntaxException { Message message = new Message(new URI("https://www.google.com"), "body", null); this.repository.insert(message); } @Test public void insertWithoutHeadersShouldSave() throws URISyntaxException { Message message = new Message(new URI("https://www.google.com"), "body", null); this.repository.insert(message); SQLiteDatabase db = this.repository.getReadableDatabase(); Cursor messageCursor = db.query("Message", new String[] {"url", "body"}, null, null, null, null, null); assertTrue(messageCursor.moveToFirst()); assertEquals(message.getUrl(), messageCursor.getString(0)); assertEquals(message.getBody(), messageCursor.getString(1)); assertTrue(messageCursor.isFirst()); assertTrue(messageCursor.isLast()); messageCursor.close(); Cursor headerCursor = db.query("Header", new String[] {"messageid", "key", "value"}, null, null, null, null, null); assertFalse(headerCursor.moveToFirst()); headerCursor.close(); } @Test public void insertWithHeadersShouldSave() throws URISyntaxException { Message message = getMessageWithHeaders(); this.repository.insert(message); assertMessage(this.repository, message); } @Test public void insertWithNullMessageAndDbSizeShouldNotThrow() { this.repository.insert(null, 4096); } @Test public void insertWithDbSizeShouldNotThrow() throws URISyntaxException { Message message = new Message(new URI("https://www.google.com"), "body", null); this.repository.insert(message, 4096); } @Test public void insertWithoutHeadersWithDbSizeShouldSave() throws URISyntaxException { Message message = new Message(new URI("https://www.google.com"), "body", null); this.repository.insert(message, 4096); SQLiteDatabase db = this.repository.getReadableDatabase(); Cursor messageCursor = db.query("Message", new String[] {"url", "body"}, null, null, null, null, null); assertTrue(messageCursor.moveToFirst()); assertEquals(message.getUrl(), messageCursor.getString(0)); assertEquals(message.getBody(), messageCursor.getString(1)); assertTrue(messageCursor.isFirst()); assertTrue(messageCursor.isLast()); messageCursor.close(); Cursor headerCursor = db.query("Header", new String[] {"messageid", "key", "value"}, null, null, null, null, null); assertFalse(headerCursor.moveToFirst()); headerCursor.close(); } @Test public void insertWithHeadersAndDbSizeShouldSave() throws URISyntaxException { Message message = getMessageWithHeaders(); this.repository.insert(message, 4096); assertMessage(this.repository, message); } @Test public void shrinkDbShouldDeleteOldMessagesIfLimitCrossed() throws URISyntaxException { SQLiteDatabase db = mock(SQLiteDatabase.class); // get db size when(db.getPageSize()).thenReturn(4096l); //get page count Cursor cursor = mock(Cursor.class); doReturn(true).when(cursor).moveToFirst(); when(cursor.getLong(0)) .thenReturn(10l) .thenReturn(1l); when(db.rawQuery("pragma page_count", null)).thenReturn(cursor); //get number of messages Cursor numberOfMessagesCursor = mock(Cursor.class); doReturn(true).when(numberOfMessagesCursor).moveToFirst(); when(numberOfMessagesCursor.getLong(0)).thenReturn(42l); when(db.rawQuery("select count(*) from Message", null)).thenReturn(numberOfMessagesCursor); //get message id to which to shrink Cursor idToShrinkCursor = mock(Cursor.class); doReturn(true).when(idToShrinkCursor).moveToFirst(); when(idToShrinkCursor.getLong(0)).thenReturn(101l); when(db.rawQuery("select id from Message limit 1 offset 32", null)).thenReturn(idToShrinkCursor); this.repository = spy(this.repository); when(this.repository.getWritableDatabase()).thenReturn(db); this.repository.shrinkDb(317200); verify(db, times(1)).execSQL("delete from Message where id < 101"); verify(db, times(1)).execSQL("delete from Header where messageid < 101"); verify(db, times(1)).execSQL("vacuum"); } @Test public void shrinkDbShouldDeleteOldRecordsMultipleTimesIfLimitCrossedAndShrinkRunsMoreThanOnce() throws URISyntaxException { SQLiteDatabase db = mock(SQLiteDatabase.class); // get db size when(db.getPageSize()).thenReturn(4096l); //get page count Cursor cursor = mock(Cursor.class); doReturn(true).when(cursor).moveToFirst(); when(cursor.getLong(0)) .thenReturn(300l) .thenReturn(5l) .thenReturn(1l); when(db.rawQuery("pragma page_count", null)).thenReturn(cursor); //get number of messages Cursor numberOfMessagesCursor = mock(Cursor.class); doReturn(true).when(numberOfMessagesCursor).moveToFirst(); when(numberOfMessagesCursor.getLong(0)).thenReturn(42l); when(db.rawQuery("select count(*) from Message", null)).thenReturn(numberOfMessagesCursor); //get message id to which to shrink Cursor idToShrinkCursor = mock(Cursor.class); doReturn(true).when(idToShrinkCursor).moveToFirst(); when(idToShrinkCursor.getLong(0)).thenReturn(101l).thenReturn(32l); when(db.rawQuery("select id from Message limit 1 offset 42", null)).thenReturn(idToShrinkCursor); when(db.rawQuery("select id from Message limit 1 offset 21", null)).thenReturn(idToShrinkCursor); this.repository = spy(this.repository); when(this.repository.getWritableDatabase()).thenReturn(db); this.repository.shrinkDb(317200); verify(db, times(1)).execSQL("delete from Message where id < 101"); verify(db, times(1)).execSQL("delete from Header where messageid < 101"); verify(db, times(1)).execSQL("delete from Message where id < 32"); verify(db, times(1)).execSQL("delete from Header where messageid < 32"); verify(db, times(1)).execSQL("vacuum"); } @Test public void insertShouldRemoveOldMessageIfApproachingLimit() throws URISyntaxException { SQLiteDatabase db = mock(SQLiteDatabase.class); // get db size when(db.insert(anyString(), anyString(), any(ContentValues.class))).thenReturn(42l); when(db.getPageSize()).thenReturn(4096l); //get page count Cursor cursor = mock(Cursor.class); when(cursor.getLong(0)).thenReturn(10l); when(db.rawQuery("pragma page_count", null)).thenReturn(cursor); //get oldest message with same type Cursor oldestMessageCursor = mock(Cursor.class); when(oldestMessageCursor.moveToNext()).thenReturn(true); when(oldestMessageCursor.getLong(0)).thenReturn(100l); when(db.query("Message", new String[]{"min(id)"}, "url=?", new String[]{"https://www.google.com"}, null, null, null)) .thenReturn(oldestMessageCursor); this.repository = spy(this.repository); when(this.repository.getWritableDatabase()).thenReturn(db); Message message = getMessageWithHeaders(); this.repository.insert(message, 41000); verify(db, times(1)).delete("Header", "messageid = ?", new String[]{"100"}); verify(db, times(1)).delete("Message", "id = ?", new String[]{"100"}); assertMockedMessage(db); } @Test public void insertWithDbSizeShouldNotDeleteIfDbSizeReachesThresholdButNoMessagesWithTheSameType() throws URISyntaxException { SQLiteDatabase db = mock(SQLiteDatabase.class); // get db size when(db.insert(anyString(), anyString(), any(ContentValues.class))).thenReturn(42l); when(db.getPageSize()).thenReturn(4096l); //get page count Cursor cursor = mock(Cursor.class); when(cursor.getLong(0)).thenReturn(10l); when(db.rawQuery("pragma page_count", null)).thenReturn(cursor); //get oldest message with same type Cursor oldestMessageCursor = mock(Cursor.class); when(oldestMessageCursor.moveToNext()).thenReturn(false); when(db.query("Message", new String[]{"min(id)"}, "url=?", new String[]{"https://www.google.com"}, null, null, null)) .thenReturn(oldestMessageCursor); this.repository = spy(this.repository); when(this.repository.getWritableDatabase()).thenReturn(db); Message message = getMessageWithHeaders(); this.repository.insert(message, 41000); verify(db, times(0)).delete("Header", "messageid = ?", new String[]{"100"}); verify(db, times(0)).delete("Message", "id = ?", new String[]{"100"}); assertMockedMessage(db); } @Test public void getNextWithNoMessagesShouldReturnNull() throws URISyntaxException { assertNull(this.repository.getNext()); } @Test public void getNextWithMessageShouldReturnIt() throws URISyntaxException { Message message = getMessageWithHeaders(); this.repository.insert(message); Message nextMessage = this.repository.getNext(); assertNotNull(nextMessage); assertNotNull(nextMessage.getId()); assertEquals(message.getUrl(), nextMessage.getUrl()); assertEquals(message.getBody(), nextMessage.getBody()); assertEquals("h1", nextMessage.getHeaders()[0].getKey()); assertEquals("v1", nextMessage.getHeaders()[0].getValue()); assertEquals("h2", nextMessage.getHeaders()[1].getKey()); assertEquals("v2", nextMessage.getHeaders()[1].getValue()); } @Test public void getNextMessageAfterGivenIdShouldReturnNextHigherMessage() throws URISyntaxException { Message message = null; for(int i = 0; i<10 ; i++) { message = getMessageWithHeadersAndNonNullId(i + 1); this.repository.insert(message); } Message nextMessage = this.repository.getNext(); Message nextMessageAfter = this.repository.getNextMessageAfter(7L); assertEquals(Long.valueOf(1), nextMessage.getId()); assertEquals(Long.valueOf(8), nextMessageAfter.getId()); } @Test public void getNextMessageAfterGivenIdShouldReturnNullIfThereIsNoMoreMessages() throws URISyntaxException { Message message = null; for(int i = 0; i<10 ; i++) { message = getMessageWithHeadersAndNonNullId(i + 1); this.repository.insert(message); } Message nextMessageAfter = this.repository.getNextMessageAfter(11L); assertNull(nextMessageAfter); } @Test public void deleteWithNullMessageShouldNotThrow() { this.repository.delete(null); } @Test public void deleteWithMessageThatHasNullIdShouldNotThrow() throws URISyntaxException { this.repository.delete(getMessageWithHeadersAndNonNullId(1)); } @Test public void deleteWithMissingMessageShouldNotThrow() throws URISyntaxException { this.repository.delete(getMessageWithHeaders()); } @Test public void deleteWithMessageShouldDelete() throws URISyntaxException { Message message = getMessageWithHeaders(); this.repository.insert(message); Message nextMessage = this.repository.getNext(); this.repository.delete(nextMessage); assertNull(this.repository.getNext()); } @Test public void shrinkDbShouldDoNothingWhenFailedToGetDatabaseSize() throws URISyntaxException { SQLiteDatabase db = mock(SQLiteDatabase.class); // get db size when(db.getPageSize()).thenReturn(4096l); //get page count Cursor cursor = mock(Cursor.class); doReturn(false).when(cursor).moveToFirst(); when(db.rawQuery("pragma page_count", null)).thenReturn(cursor); this.repository = spy(this.repository); when(this.repository.getWritableDatabase()).thenReturn(db); this.repository.shrinkDb(317200); verify(db, times(0)).execSQL("delete from Message where id < 101"); verify(db, times(0)).execSQL("delete from Header where messageid < 101"); verify(db, times(0)).execSQL("delete from Message where id < 32"); verify(db, times(0)).execSQL("delete from Header where messageid < 32"); verify(db, times(0)).execSQL("vacuum"); verify(cursor, times(1)).close(); verify(db, times(1)).close(); } @Test public void shrinkDbShouldDoNothingWhenFailedToGetNumberOfMessages() throws URISyntaxException { SQLiteDatabase db = mock(SQLiteDatabase.class); // get db size when(db.getPageSize()).thenReturn(4096l); //get page count Cursor cursor = mock(Cursor.class); doReturn(true).when(cursor).moveToFirst(); when(cursor.getLong(0)) .thenReturn(300l) .thenReturn(5l) .thenReturn(1l); when(db.rawQuery("pragma page_count", null)).thenReturn(cursor); //get number of messages Cursor numberOfMessagesCursor = mock(Cursor.class); doReturn(false).when(numberOfMessagesCursor).moveToFirst(); when(db.rawQuery("select count(*) from Message", null)).thenReturn(numberOfMessagesCursor); //get message id to which to shrink Cursor idToShrinkCursor = mock(Cursor.class); doReturn(true).when(idToShrinkCursor).moveToFirst(); when(idToShrinkCursor.getLong(0)).thenReturn(101l).thenReturn(32l); when(db.rawQuery("select id from Message limit 1 offset 1", null)).thenReturn(idToShrinkCursor); this.repository = spy(this.repository); when(this.repository.getWritableDatabase()).thenReturn(db); this.repository.shrinkDb(317200); verify(db, times(1)).execSQL("delete from Message where id < 101"); verify(db, times(1)).execSQL("delete from Header where messageid < 101"); verify(db, times(1)).execSQL("delete from Message where id < 32"); verify(db, times(1)).execSQL("delete from Header where messageid < 32"); verify(db, times(1)).execSQL("vacuum"); verify(cursor, times(3)).close(); verify(numberOfMessagesCursor, times(2)).close(); verify(idToShrinkCursor, times(2)).close(); verify(db, times(1)).close(); } @Test public void deleteMessagesFromShouldDeleteOnlyMessageLessThanOrEqualGivenId() throws URISyntaxException { Message message = null; for(int i = 0; i<10 ; i++) { message = getMessageWithHeadersAndNonNullId(i + 1); this.repository.insert(message); } this.repository.deleteMessagesFrom(7); Message nextMessage = this.repository.getNext(); assertEquals(Long.valueOf(8), nextMessage.getId()); } @Test public void deleteMessagesFromShouldDeleteHeadersRelatedToMessages() throws URISyntaxException { Message message = null; for(int i = 0; i<10 ; i++) { message = getMessageWithHeadersAndNonNullId(i + 1); this.repository.insert(message); } this.repository.deleteMessagesFrom(7); SQLiteDatabase db = repository.getReadableDatabase(); Cursor headerCursor = db.query("Header", new String[] {"messageid", "key", "value"}, null, null, null, null, "key"); assertTrue(headerCursor.moveToFirst()); assertEquals(8, headerCursor.getLong(0)); headerCursor.close(); } @Test public void getAvailableMessagesCountShouldGetRowsCount() throws URISyntaxException { SQLiteDatabase db = mock(SQLiteDatabase.class); //get number of messages Cursor numberOfMessagesCursor = mock(Cursor.class); doReturn(true).when(numberOfMessagesCursor).moveToFirst(); when(numberOfMessagesCursor.getLong(0)).thenReturn(7l); when(db.rawQuery("select count(*) from Message", null)).thenReturn(numberOfMessagesCursor); this.repository = spy(this.repository); when(this.repository.getReadableDatabase()).thenReturn(db); long actual = this.repository.getAvailableMessagesCount(); verify(db).rawQuery("select count(*) from Message", null); assertEquals(7L, actual); verify(numberOfMessagesCursor).close(); } public static Message getMessageWithHeaders() throws URISyntaxException { return getMessageWithHeaders("body"); } public static Message getMessageWithHeaders(String body) throws URISyntaxException { return new Message(new URI("https://www.google.com"), body, new Header[] { new Header("h1", "v1"), new Header("h2", "v2") }); } public static Message getMessageWithGzipHeaders(String body) throws URISyntaxException { return new Message(new URI("https://www.google.com"), body, new Header[] { new Header("h1", "v1"), new Header("h2", "v2"), new Header("content-encoding", "gzip") }); } public static Message getMessageWithHeadersAndNonNullId(long id) throws URISyntaxException { return new Message(id, new URI("https://www.google.com"), "body", new Header[] { new Header("h1", "v1"), new Header("h2", "v2") }); } private static void assertMessage(Repository repository, Message message) { SQLiteDatabase db = repository.getReadableDatabase(); Cursor messageCursor = db.query("Message", new String[] {"id", "url", "body"}, null, null, null, null, null); assertTrue(messageCursor.moveToFirst()); assertEquals(message.getUrl(), messageCursor.getString(1)); assertEquals(message.getBody(), messageCursor.getString(2)); assertTrue(messageCursor.isFirst()); assertTrue(messageCursor.isLast()); Cursor headerCursor = db.query("Header", new String[] {"messageid", "key", "value"}, null, null, null, null, "key"); assertTrue(headerCursor.moveToFirst()); assertEquals(messageCursor.getLong(0), headerCursor.getLong(0)); assertEquals("h1", headerCursor.getString(1)); assertEquals("v1", headerCursor.getString(2)); assertTrue(headerCursor.isFirst()); assertTrue(headerCursor.moveToNext()); assertEquals(messageCursor.getLong(0), headerCursor.getLong(0)); assertEquals("h2", headerCursor.getString(1)); assertEquals("v2", headerCursor.getString(2)); assertTrue(headerCursor.isLast()); messageCursor.close(); headerCursor.close(); db.close(); } private static void assertMockedMessage(SQLiteDatabase mockedDb) { verify(mockedDb, times(3)).insert(anyString(), anyString(), any(ContentValues.class)); } }