diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index 7f7dd2d569..62f069b3ca 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -22,7 +22,7 @@ import dagger.android.DaggerApplication; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.ContributionDao; -import fr.free.nrw.commons.data.Category; +import fr.free.nrw.commons.data.CategoryDao; import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.di.CommonsApplicationComponent; import fr.free.nrw.commons.di.CommonsApplicationModule; @@ -49,15 +49,15 @@ public class CommonsApplication extends DaggerApplication { @Inject @Named("default_preferences") SharedPreferences defaultPrefs; @Inject @Named("application_preferences") SharedPreferences applicationPrefs; @Inject @Named("prefs") SharedPreferences otherPrefs; - + public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app"; - + public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com"; - + public static final String LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com"; - + public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback"; - + private CommonsApplicationComponent component; private RefWatcher refWatcher; @@ -95,7 +95,7 @@ protected RefWatcher setupLeakCanary() { } return LeakCanary.install(this); } - + /** * Provides a way to get member refWatcher * @@ -106,7 +106,7 @@ public static RefWatcher getRefWatcher(Context context) { CommonsApplication application = (CommonsApplication) context.getApplicationContext(); return application.refWatcher; } - + /** * Helps in injecting dependency library Dagger * @return Dagger injector @@ -169,7 +169,7 @@ private void updateAllDatabases() { SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); ModifierSequence.Table.onDelete(db); - Category.Table.onDelete(db); + CategoryDao.Table.onDelete(db); ContributionDao.Table.onDelete(db); } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java index 0c8e2a877e..335f7364ca 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java @@ -37,6 +37,7 @@ import dagger.android.support.DaggerFragment; import fr.free.nrw.commons.R; import fr.free.nrw.commons.data.Category; +import fr.free.nrw.commons.data.CategoryDao; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.upload.MwVolleyApi; import fr.free.nrw.commons.utils.StringSortingUtils; @@ -79,7 +80,7 @@ public class CategorizationFragment extends DaggerFragment { private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> { if (item.isSelected()) { selectedCategories.add(item); - updateCategoryCount(item, databaseClient); + updateCategoryCount(item); } else { selectedCategories.remove(item); } @@ -261,7 +262,7 @@ private Observable titleCategories() { } private Observable recentCategories() { - return Observable.fromIterable(Category.recentCategories(databaseClient, SEARCH_CATS_LIMIT)) + return Observable.fromIterable(new CategoryDao(databaseClient).recentCategories(SEARCH_CATS_LIMIT)) .map(s -> new CategoryItem(s, false)); } @@ -311,24 +312,17 @@ private boolean containsYear(String item) { || item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")); } - private void updateCategoryCount(CategoryItem item, ContentProviderClient client) { - Category cat = lookupCategory(item.getName()); - cat.incTimesUsed(); - cat.save(client); - } - - private Category lookupCategory(String name) { - Category cat = Category.find(databaseClient, name); + private void updateCategoryCount(CategoryItem item) { + CategoryDao categoryDao = new CategoryDao(databaseClient); + Category category = categoryDao.find(item.getName()); - if (cat == null) { - // Newly used category... - cat = new Category(); - cat.setName(name); - cat.setLastUsed(new Date()); - cat.setTimesUsed(0); + // Newly used category... + if (category == null) { + category = new Category(null, item.getName(), new Date(), 0); } - return cat; + category.incTimesUsed(); + categoryDao.save(category); } public int getCurrentSelectedCount() { diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java index ed698ec4c5..3384a984b4 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java @@ -17,9 +17,9 @@ import timber.log.Timber; import static android.content.UriMatcher.NO_MATCH; -import static fr.free.nrw.commons.data.Category.Table.ALL_FIELDS; -import static fr.free.nrw.commons.data.Category.Table.COLUMN_ID; -import static fr.free.nrw.commons.data.Category.Table.TABLE_NAME; +import static fr.free.nrw.commons.data.CategoryDao.Table.ALL_FIELDS; +import static fr.free.nrw.commons.data.CategoryDao.Table.COLUMN_ID; +import static fr.free.nrw.commons.data.CategoryDao.Table.TABLE_NAME; public class CategoryContentProvider extends ContentProvider { diff --git a/app/src/main/java/fr/free/nrw/commons/data/Category.java b/app/src/main/java/fr/free/nrw/commons/data/Category.java index be4a298465..9a32f3a7a3 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/Category.java +++ b/app/src/main/java/fr/free/nrw/commons/data/Category.java @@ -1,30 +1,28 @@ package fr.free.nrw.commons.data; -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.net.Uri; -import android.os.RemoteException; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import java.util.ArrayList; import java.util.Date; -import fr.free.nrw.commons.category.CategoryContentProvider; - /** * Represents a category */ public class Category { private Uri contentUri; - private String name; private Date lastUsed; private int timesUsed; - // Getters/setters + public Category() { + } + + public Category(Uri contentUri, String name, Date lastUsed, int timesUsed) { + this.contentUri = contentUri; + this.name = name; + this.lastUsed = lastUsed; + this.timesUsed = timesUsed; + } + /** * Gets name * @@ -48,21 +46,11 @@ public void setName(String name) { * * @return Last used date */ - private Date getLastUsed() { + public Date getLastUsed() { // warning: Date objects are mutable. return (Date)lastUsed.clone(); } - /** - * Modifies last used date - * - * @param lastUsed Category date - */ - public void setLastUsed(Date lastUsed) { - // warning: Date objects are mutable. - this.lastUsed = (Date)lastUsed.clone(); - } - /** * Generates new last used date */ @@ -75,19 +63,10 @@ private void touch() { * * @return no. of times used */ - private int getTimesUsed() { + public int getTimesUsed() { return timesUsed; } - /** - * Modifies no. of times used - * - * @param timesUsed Category used times - */ - public void setTimesUsed(int timesUsed) { - this.timesUsed = timesUsed; - } - /** * Increments timesUsed by 1 and sets last used date as now. */ @@ -96,181 +75,22 @@ public void incTimesUsed() { touch(); } - //region Database/content-provider stuff - /** - * Persist category. - * @param client ContentProviderClient to handle DB connection - */ - public void save(ContentProviderClient client) { - try { - if (contentUri == null) { - contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues()); - } else { - client.update(contentUri, toContentValues(), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - - /** - * Gets content values + * Gets the content URI for this category * - * @return Content values - */ - private ContentValues toContentValues() { - ContentValues cv = new ContentValues(); - cv.put(Table.COLUMN_NAME, getName()); - cv.put(Table.COLUMN_LAST_USED, getLastUsed().getTime()); - cv.put(Table.COLUMN_TIMES_USED, getTimesUsed()); - return cv; - } - - /** - * Gets category from cursor - * @param cursor Category cursor - * @return Category from cursor - */ - private static Category fromCursor(Cursor cursor) { - // Hardcoding column positions! - Category c = new Category(); - c.contentUri = CategoryContentProvider.uriForId(cursor.getInt(0)); - c.name = cursor.getString(1); - c.lastUsed = new Date(cursor.getLong(2)); - c.timesUsed = cursor.getInt(3); - return c; - } - - /** - * Find persisted category in database, based on its name. - * @param client ContentProviderClient to handle DB connection - * @param name Category's name - * @return category from database, or null if not found + * @return content URI */ - public static @Nullable Category find(ContentProviderClient client, String name) { - Cursor cursor = null; - try { - cursor = client.query( - CategoryContentProvider.BASE_URI, - Category.Table.ALL_FIELDS, - Category.Table.COLUMN_NAME + "=?", - new String[]{name}, - null); - if (cursor != null && cursor.moveToFirst()) { - return Category.fromCursor(cursor); - } - } catch (RemoteException e) { - // This feels lazy, but to hell with checked exceptions. :) - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - return null; + public Uri getContentUri() { + return contentUri; } /** - * Retrieve recently-used categories, ordered by descending date. - * @return a list containing recent categories + * Modifies the content URI - marking this category as already saved in the database + * + * @param contentUri the content URI */ - public static @NonNull ArrayList recentCategories(ContentProviderClient client, int limit) { - ArrayList items = new ArrayList<>(); - Cursor cursor = null; - try { - cursor = client.query( - CategoryContentProvider.BASE_URI, - Category.Table.ALL_FIELDS, - null, - new String[]{}, - Category.Table.COLUMN_LAST_USED + " DESC"); - // fixme add a limit on the original query instead of falling out of the loop? - while (cursor != null && cursor.moveToNext() - && cursor.getPosition() < limit) { - Category cat = Category.fromCursor(cursor); - items.add(cat.getName()); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - return items; + public void setContentUri(Uri contentUri) { + this.contentUri = contentUri; } - public static class Table { - public static final String TABLE_NAME = "categories"; - - public static final String COLUMN_ID = "_id"; - public static final String COLUMN_NAME = "name"; - public static final String COLUMN_LAST_USED = "last_used"; - public static final String COLUMN_TIMES_USED = "times_used"; - - // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. - public static final String[] ALL_FIELDS = { - COLUMN_ID, - COLUMN_NAME, - COLUMN_LAST_USED, - COLUMN_TIMES_USED - }; - - private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + COLUMN_ID + " INTEGER PRIMARY KEY," - + COLUMN_NAME + " STRING," - + COLUMN_LAST_USED + " INTEGER," - + COLUMN_TIMES_USED + " INTEGER" - + ");"; - - /** - * Creates new table with provided SQLite database - * - * @param db Category database - */ - public static void onCreate(SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - /** - * Deletes existing table - * @param db Category database - */ - public static void onDelete(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); - onCreate(db); - } - - /** - * Updates given database - * @param db Category database - * @param from Exiting category id - * @param to New category id - */ - public static void onUpdate(SQLiteDatabase db, int from, int to) { - if (from == to) { - return; - } - if (from < 4) { - // doesn't exist yet - from++; - onUpdate(db, from, to); - return; - } - if (from == 4) { - // table added in version 5 - onCreate(db); - from++; - onUpdate(db, from, to); - return; - } - if (from == 5) { - from++; - onUpdate(db, from, to); - return; - } - } - } - //endregion } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/data/CategoryDao.java b/app/src/main/java/fr/free/nrw/commons/data/CategoryDao.java new file mode 100644 index 0000000000..8bae4a522b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/data/CategoryDao.java @@ -0,0 +1,174 @@ +package fr.free.nrw.commons.data; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.os.RemoteException; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import fr.free.nrw.commons.category.CategoryContentProvider; + +public class CategoryDao { + + private final ContentProviderClient client; + + public CategoryDao(ContentProviderClient client) { + this.client = client; + } + + public void save(Category category) { + try { + if (category.getContentUri() == null) { + category.setContentUri(client.insert(CategoryContentProvider.BASE_URI, toContentValues(category))); + } else { + client.update(category.getContentUri(), toContentValues(category), null, null); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Find persisted category in database, based on its name. + * + * @param name Category's name + * @return category from database, or null if not found + */ + public @Nullable + Category find(String name) { + Cursor cursor = null; + try { + cursor = client.query( + CategoryContentProvider.BASE_URI, + Table.ALL_FIELDS, + Table.COLUMN_NAME + "=?", + new String[]{name}, + null); + if (cursor != null && cursor.moveToFirst()) { + return fromCursor(cursor); + } + } catch (RemoteException e) { + // This feels lazy, but to hell with checked exceptions. :) + throw new RuntimeException(e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + /** + * Retrieve recently-used categories, ordered by descending date. + * + * @return a list containing recent categories + */ + public @NonNull + List recentCategories(int limit) { + List items = new ArrayList<>(); + Cursor cursor = null; + try { + cursor = client.query( + CategoryContentProvider.BASE_URI, + Table.ALL_FIELDS, + null, + new String[]{}, + Table.COLUMN_LAST_USED + " DESC"); + // fixme add a limit on the original query instead of falling out of the loop? + while (cursor != null && cursor.moveToNext() + && cursor.getPosition() < limit) { + items.add(fromCursor(cursor).getName()); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return items; + } + + Category fromCursor(Cursor cursor) { + // Hardcoding column positions! + return new Category( + CategoryContentProvider.uriForId(cursor.getInt(0)), + cursor.getString(1), + new Date(cursor.getLong(2)), + cursor.getInt(3) + ); + } + + private ContentValues toContentValues(Category category) { + ContentValues cv = new ContentValues(); + cv.put(CategoryDao.Table.COLUMN_NAME, category.getName()); + cv.put(CategoryDao.Table.COLUMN_LAST_USED, category.getLastUsed().getTime()); + cv.put(CategoryDao.Table.COLUMN_TIMES_USED, category.getTimesUsed()); + return cv; + } + + public static class Table { + public static final String TABLE_NAME = "categories"; + + public static final String COLUMN_ID = "_id"; + static final String COLUMN_NAME = "name"; + static final String COLUMN_LAST_USED = "last_used"; + static final String COLUMN_TIMES_USED = "times_used"; + + // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. + public static final String[] ALL_FIELDS = { + COLUMN_ID, + COLUMN_NAME, + COLUMN_LAST_USED, + COLUMN_TIMES_USED + }; + + static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; + + static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" + + COLUMN_ID + " INTEGER PRIMARY KEY," + + COLUMN_NAME + " STRING," + + COLUMN_LAST_USED + " INTEGER," + + COLUMN_TIMES_USED + " INTEGER" + + ");"; + + public static void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_STATEMENT); + } + + public static void onDelete(SQLiteDatabase db) { + db.execSQL(DROP_TABLE_STATEMENT); + onCreate(db); + } + + static void onUpdate(SQLiteDatabase db, int from, int to) { + if (from == to) { + return; + } + if (from < 4) { + // doesn't exist yet + from++; + onUpdate(db, from, to); + return; + } + if (from == 4) { + // table added in version 5 + onCreate(db); + from++; + onUpdate(db, from, to); + return; + } + if (from == 5) { + from++; + onUpdate(db, from, to); + return; + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java index 9e4608e466..c4b4a5258a 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java @@ -23,13 +23,13 @@ public DBOpenHelper(Context context) { public void onCreate(SQLiteDatabase sqLiteDatabase) { ContributionDao.Table.onCreate(sqLiteDatabase); ModifierSequence.Table.onCreate(sqLiteDatabase); - Category.Table.onCreate(sqLiteDatabase); + CategoryDao.Table.onCreate(sqLiteDatabase); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) { ContributionDao.Table.onUpdate(sqLiteDatabase, from, to); ModifierSequence.Table.onUpdate(sqLiteDatabase, from, to); - Category.Table.onUpdate(sqLiteDatabase, from, to); + CategoryDao.Table.onUpdate(sqLiteDatabase, from, to); } } diff --git a/app/src/test/java/fr/free/nrw/commons/data/CategoryDaoTest.java b/app/src/test/java/fr/free/nrw/commons/data/CategoryDaoTest.java new file mode 100644 index 0000000000..62d4f4d2cb --- /dev/null +++ b/app/src/test/java/fr/free/nrw/commons/data/CategoryDaoTest.java @@ -0,0 +1,295 @@ +package fr.free.nrw.commons.data; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.RemoteException; +import android.support.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.TestCommonsApplication; +import fr.free.nrw.commons.category.CategoryContentProvider; +import fr.free.nrw.commons.data.CategoryDao.Table; + +import static fr.free.nrw.commons.category.CategoryContentProvider.BASE_URI; +import static fr.free.nrw.commons.category.CategoryContentProvider.uriForId; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class) +public class CategoryDaoTest { + + @Mock + ContentProviderClient client; + @Mock + SQLiteDatabase database; + @Captor + ArgumentCaptor captor; + @Captor + ArgumentCaptor queryCaptor; + + private CategoryDao testObject; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + testObject = new CategoryDao(client); + } + + @Test + public void createTable() { + Table.onCreate(database); + verify(database).execSQL(Table.CREATE_TABLE_STATEMENT); + } + + @Test + public void deleteTable() { + Table.onDelete(database); + InOrder inOrder = Mockito.inOrder(database); + inOrder.verify(database).execSQL(Table.DROP_TABLE_STATEMENT); + inOrder.verify(database).execSQL(Table.CREATE_TABLE_STATEMENT); + } + + @Test + public void migrateTableVersionFrom_v1_to_v2() { + Table.onUpdate(database, 1, 2); + // Table didnt exist before v5 + verifyZeroInteractions(database); + } + + @Test + public void migrateTableVersionFrom_v2_to_v3() { + Table.onUpdate(database, 2, 3); + // Table didnt exist before v5 + verifyZeroInteractions(database); + } + + @Test + public void migrateTableVersionFrom_v3_to_v4() { + Table.onUpdate(database, 3, 4); + // Table didnt exist before v5 + verifyZeroInteractions(database); + } + + @Test + public void migrateTableVersionFrom_v4_to_v5() { + Table.onUpdate(database, 4, 5); + verify(database).execSQL(Table.CREATE_TABLE_STATEMENT); + } + + @Test + public void migrateTableVersionFrom_v5_to_v6() { + Table.onUpdate(database, 5, 6); + // Table didnt change in version 6 + verifyZeroInteractions(database); + } + + @Test + public void createFromCursor() { + MatrixCursor cursor = createCursor(1); + cursor.moveToFirst(); + Category category = testObject.fromCursor(cursor); + + assertEquals(uriForId(1), category.getContentUri()); + assertEquals("foo", category.getName()); + assertEquals(123, category.getLastUsed().getTime()); + assertEquals(2, category.getTimesUsed()); + } + + @Test + public void saveExistingCategory() throws Exception { + MatrixCursor cursor = createCursor(1); + cursor.moveToFirst(); + Category category = testObject.fromCursor(cursor); + + testObject.save(category); + + verify(client).update(eq(category.getContentUri()), captor.capture(), isNull(String.class), isNull(String[].class)); + ContentValues cv = captor.getValue(); + assertEquals(3, cv.size()); + assertEquals(category.getName(), cv.getAsString(Table.COLUMN_NAME)); + assertEquals(category.getLastUsed().getTime(), cv.getAsLong(Table.COLUMN_LAST_USED).longValue()); + assertEquals(category.getTimesUsed(), cv.getAsInteger(Table.COLUMN_TIMES_USED).intValue()); + } + + @Test + public void saveNewCategory() throws Exception { + Uri contentUri = CategoryContentProvider.uriForId(111); + when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); + Category category = new Category(null, "foo", new Date(234L), 1); + + testObject.save(category); + + verify(client).insert(eq(BASE_URI), captor.capture()); + ContentValues cv = captor.getValue(); + assertEquals(3, cv.size()); + assertEquals(category.getName(), cv.getAsString(Table.COLUMN_NAME)); + assertEquals(category.getLastUsed().getTime(), cv.getAsLong(Table.COLUMN_LAST_USED).longValue()); + assertEquals(category.getTimesUsed(), cv.getAsInteger(Table.COLUMN_TIMES_USED).intValue()); + assertEquals(contentUri, category.getContentUri()); + } + + @Test(expected = RuntimeException.class) + public void testSaveTranslatesRemoteExceptions() throws Exception { + when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenThrow(new RemoteException("")); + testObject.save(new Category()); + } + + @Test + public void whenTheresNoDataFindReturnsNull_nullCursor() throws Exception { + when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(null); + + assertNull(testObject.find("foo")); + } + + @Test + public void whenTheresNoDataFindReturnsNull_emptyCursor() throws Exception { + when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(createCursor(0)); + + assertNull(testObject.find("foo")); + } + + @Test + public void cursorsAreClosedAfterUse() throws Exception { + Cursor mockCursor = mock(Cursor.class); + when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(mockCursor); + when(mockCursor.moveToFirst()).thenReturn(false); + + testObject.find("foo"); + + verify(mockCursor).close(); + } + + @Test + public void findCategory() throws Exception { + when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(createCursor(1)); + + Category category = testObject.find("foo"); + + assertEquals(uriForId(1), category.getContentUri()); + assertEquals("foo", category.getName()); + assertEquals(123, category.getLastUsed().getTime()); + assertEquals(2, category.getTimesUsed()); + + verify(client).query( + eq(BASE_URI), + eq(Table.ALL_FIELDS), + eq(Table.COLUMN_NAME + "=?"), + queryCaptor.capture(), + isNull(String.class) + ); + assertEquals("foo", queryCaptor.getValue()[0]); + } + + @Test(expected = RuntimeException.class) + public void findCategoryTranslatesExceptions() throws Exception { + when(client.query(any(), any(), anyString(), any(), anyString())).thenThrow(new RemoteException("")); + testObject.find("foo"); + } + + @Test(expected = RuntimeException.class) + public void recentCategoriesTranslatesExceptions() throws Exception { + when(client.query(any(), any(), anyString(), any(), anyString())).thenThrow(new RemoteException("")); + testObject.recentCategories(1); + } + + @Test + public void recentCategoriesReturnsEmptyList_nullCursor() throws Exception { + when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(null); + + assertTrue(testObject.recentCategories(1).isEmpty()); + } + + @Test + public void recentCategoriesReturnsEmptyList_emptyCursor() throws Exception { + when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(createCursor(0)); + + assertTrue(testObject.recentCategories(1).isEmpty()); + } + + @Test + public void cursorsAreClosedAfterRecentCategoriesQuery() throws Exception { + Cursor mockCursor = mock(Cursor.class); + when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(mockCursor); + when(mockCursor.moveToFirst()).thenReturn(false); + + testObject.recentCategories(1); + + verify(mockCursor).close(); + } + + @Test + public void recentCategoriesReturnsLessThanLimit() throws Exception { + when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(createCursor(1)); + + List result = testObject.recentCategories(10); + + assertEquals(1, result.size()); + assertEquals("foo", result.get(0)); + + verify(client).query( + eq(BASE_URI), + eq(Table.ALL_FIELDS), + isNull(String.class), + queryCaptor.capture(), + eq(Table.COLUMN_LAST_USED + " DESC") + ); + assertEquals(0, queryCaptor.getValue().length); + } + + @Test + public void recentCategoriesHomorsLimit() throws Exception { + when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(createCursor(10)); + + List result = testObject.recentCategories(5); + + assertEquals(5, result.size()); + } + + @NonNull + private MatrixCursor createCursor(int rowCount) { + MatrixCursor cursor = new MatrixCursor(new String[]{ + Table.COLUMN_ID, + Table.COLUMN_NAME, + Table.COLUMN_LAST_USED, + Table.COLUMN_TIMES_USED + }, rowCount); + + for (int i = 0; i < rowCount; i++) { + cursor.addRow(Arrays.asList("1", "foo", "123", "2")); + } + + return cursor; + } + +} \ No newline at end of file