From dac3657536b0a258b29056e6022a399956dfc939 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Thu, 28 Nov 2024 23:01:29 -0600 Subject: [PATCH] Migrate kvstore to kotlin (#5973) * Get good test around the basic KvStore before starting conversion * Converted BasicKvStore to kotlin and removed dead code * Converted JsonKvStore to kotlin --------- Co-authored-by: Nicolas Raoul --- .../free/nrw/commons/auth/SessionManager.kt | 2 +- .../nrw/commons/kvstore/BasicKvStore.java | 215 ---------------- .../free/nrw/commons/kvstore/BasicKvStore.kt | 152 +++++++++++ .../free/nrw/commons/kvstore/JsonKvStore.java | 68 ----- .../free/nrw/commons/kvstore/JsonKvStore.kt | 52 ++++ .../nrw/commons/kvstore/KeyValueStore.java | 35 --- .../free/nrw/commons/kvstore/KeyValueStore.kt | 33 +++ .../nrw/commons/utils/SystemThemeUtils.kt | 2 +- .../nrw/commons/kvstore/BasicKvStoreTest.kt | 238 ++++++++++++++++++ .../nrw/commons/kvstore/JsonKvStoreTest.kt | 85 +++++++ 10 files changed, 562 insertions(+), 320 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.java create mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.java create mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.java create mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/kvstore/BasicKvStoreTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/kvstore/JsonKvStoreTest.kt diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt index eba4a55f4..c9eb7d2f1 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt @@ -62,7 +62,7 @@ class SessionManager @Inject constructor( fun forceLogin(context: Context?) = context?.let { LoginActivity.startYourself(it) } - fun getPreference(key: String?): Boolean = + fun getPreference(key: String): Boolean = defaultKvStore.getBoolean(key) fun logout(): Completable = Completable.fromObservable( diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.java b/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.java deleted file mode 100644 index 032898896..000000000 --- a/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.java +++ /dev/null @@ -1,215 +0,0 @@ -package fr.free.nrw.commons.kvstore; - -import android.content.Context; -import android.content.SharedPreferences; - -import androidx.annotation.Nullable; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import timber.log.Timber; - -public class BasicKvStore implements KeyValueStore { - private static final String KEY_VERSION = "__version__"; - /* - This class only performs puts, sets and clears. - A commit returns a boolean indicating whether it has succeeded, we are not throwing an exception as it will - require the dev to handle it in every usage - instead we will pass on this boolean so it can be evaluated if needed. - */ - private final SharedPreferences _store; - - public BasicKvStore(Context context, String storeName) { - _store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE); - } - - /** - * If you don't want onVersionUpdate to be called on a fresh creation, the first version supplied for the kvstore should be set to 0. - */ - public BasicKvStore(Context context, String storeName, int version) { - this(context,storeName,version,false); - } - - public BasicKvStore(Context context, String storeName, int version, boolean clearAllOnUpgrade) { - _store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE); - int oldVersion = getInt(KEY_VERSION); - - if (version > oldVersion) { - Timber.i("version updated from %s to %s, with clearFlag %b", oldVersion, version, clearAllOnUpgrade); - onVersionUpdate(oldVersion, version, clearAllOnUpgrade); - } - - if (version < oldVersion) { - throw new IllegalArgumentException( - "kvstore downgrade not allowed, old version:" + oldVersion + ", new version: " + - version); - } - //Keep this statement at the end so that clearing of store does not cause version also to get removed. - putIntInternal(KEY_VERSION, version); - } - - public void onVersionUpdate(int oldVersion, int version, boolean clearAllFlag) { - if(clearAllFlag) { - clearAll(); - } - } - - public Set getKeySet() { - Map allContents = new HashMap<>(_store.getAll()); - allContents.remove(KEY_VERSION); - return allContents.keySet(); - } - - @Nullable - public Map getAll() { - Map allContents = _store.getAll(); - if (allContents == null || allContents.size() == 0) { - return null; - } - allContents.remove(KEY_VERSION); - return new HashMap<>(allContents); - } - - @Override - public String getString(String key) { - return getString(key, null); - } - - @Override - public boolean getBoolean(String key) { - return getBoolean(key, false); - } - - @Override - public long getLong(String key) { - return getLong(key, 0); - } - - @Override - public int getInt(String key) { - return getInt(key, 0); - } - - @Override - public String getString(String key, String defaultValue) { - return _store.getString(key, defaultValue); - } - - @Override - public boolean getBoolean(String key, boolean defaultValue) { - return _store.getBoolean(key, defaultValue); - } - - @Override - public long getLong(String key, long defaultValue) { - return _store.getLong(key, defaultValue); - } - - @Override - public int getInt(String key, int defaultValue) { - return _store.getInt(key, defaultValue); - } - - public void putAllStrings(Map keyValuePairs) { - SharedPreferences.Editor editor = _store.edit(); - for (Map.Entry keyValuePair : keyValuePairs.entrySet()) { - putString(editor, keyValuePair.getKey(), keyValuePair.getValue(), false); - } - editor.apply(); - } - - @Override - public void putString(String key, String value) { - SharedPreferences.Editor editor = _store.edit(); - putString(editor, key, value, true); - } - - private void putString(SharedPreferences.Editor editor, String key, String value, - boolean commit) { - assertKeyNotReserved(key); - editor.putString(key, value); - if(commit) { - editor.apply(); - } - } - - @Override - public void putBoolean(String key, boolean value) { - assertKeyNotReserved(key); - SharedPreferences.Editor editor = _store.edit(); - editor.putBoolean(key, value); - editor.apply(); - } - - @Override - public void putLong(String key, long value) { - assertKeyNotReserved(key); - SharedPreferences.Editor editor = _store.edit(); - editor.putLong(key, value); - editor.apply(); - } - - @Override - public void putInt(String key, int value) { - assertKeyNotReserved(key); - putIntInternal(key, value); - } - - @Override - public boolean contains(String key) { - return _store.contains(key); - } - - @Override - public void remove(String key) { - SharedPreferences.Editor editor = _store.edit(); - editor.remove(key); - editor.apply(); - } - - @Override - public void clearAll() { - int version = getInt(KEY_VERSION); - SharedPreferences.Editor editor = _store.edit(); - editor.clear(); - editor.apply(); - putIntInternal(KEY_VERSION, version); - } - - @Override - public void clearAllWithVersion() { - SharedPreferences.Editor editor = _store.edit(); - editor.clear(); - editor.apply(); - } - - private void putIntInternal(String key, int value) { - SharedPreferences.Editor editor = _store.edit(); - editor.putInt(key, value); - editor.apply(); - } - - private void assertKeyNotReserved(String key) { - if (key.equals(KEY_VERSION)) { - throw new IllegalArgumentException(key + "is a reserved key"); - } - } - - public void registerChangeListener(SharedPreferences.OnSharedPreferenceChangeListener l) { - _store.registerOnSharedPreferenceChangeListener(l); - } - - public void unregisterChangeListener(SharedPreferences.OnSharedPreferenceChangeListener l) { - _store.unregisterOnSharedPreferenceChangeListener(l); - } - - public Set getStringSet(String key){ - return _store.getStringSet(key, new HashSet<>()); - } - - public void putStringSet(String key,Set value){ - _store.edit().putStringSet(key,value).apply(); - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt b/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt new file mode 100644 index 000000000..e0b860164 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt @@ -0,0 +1,152 @@ +package fr.free.nrw.commons.kvstore + +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.VisibleForTesting +import androidx.core.content.edit +import timber.log.Timber + +open class BasicKvStore : KeyValueStore { + /* + This class only performs puts, sets and clears. + A commit returns a boolean indicating whether it has succeeded, we are not throwing an exception as it will + require the dev to handle it in every usage - instead we will pass on this boolean so it can be evaluated if needed. + */ + private val _store: SharedPreferences + + constructor(context: Context, storeName: String?) { + _store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE) + } + + /** + * If you don't want onVersionUpdate to be called on a fresh creation, the first version supplied for the kvstore should be set to 0. + */ + @JvmOverloads + constructor( + context: Context, + storeName: String?, + version: Int, + clearAllOnUpgrade: Boolean = false + ) { + _store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE) + val oldVersion = _store.getInt(KEY_VERSION, 0) + + require(version >= oldVersion) { + "kvstore downgrade not allowed, old version:" + oldVersion + ", new version: " + + version + } + + if (version > oldVersion) { + Timber.i( + "version updated from %s to %s, with clearFlag %b", + oldVersion, + version, + clearAllOnUpgrade + ) + onVersionUpdate(oldVersion, version, clearAllOnUpgrade) + } + + //Keep this statement at the end so that clearing of store does not cause version also to get removed. + _store.edit { putInt(KEY_VERSION, version) } + } + + val all: Map? + get() { + val allContents = _store.all + if (allContents == null || allContents.isEmpty()) { + return null + } + allContents.remove(KEY_VERSION) + return HashMap(allContents) + } + + override fun getString(key: String): String? = + getString(key, null) + + override fun getBoolean(key: String): Boolean = + getBoolean(key, false) + + override fun getLong(key: String): Long = + getLong(key, 0) + + override fun getInt(key: String): Int = + getInt(key, 0) + + fun getStringSet(key: String?): MutableSet = + _store.getStringSet(key, HashSet())!! + + override fun getString(key: String, defaultValue: String?): String? = + _store.getString(key, defaultValue) + + override fun getBoolean(key: String, defaultValue: Boolean): Boolean = + _store.getBoolean(key, defaultValue) + + override fun getLong(key: String, defaultValue: Long): Long = + _store.getLong(key, defaultValue) + + override fun getInt(key: String, defaultValue: Int): Int = + _store.getInt(key, defaultValue) + + fun putAllStrings(kvData: Map) = assertKeyNotReserved(kvData.keys) { + for ((key, value) in kvData) { + putString(key, value) + } + } + + override fun putString(key: String, value: String) = assertKeyNotReserved(key) { + putString(key, value) + } + + override fun putBoolean(key: String, value: Boolean) = assertKeyNotReserved(key) { + putBoolean(key, value) + } + + override fun putLong(key: String, value: Long) = assertKeyNotReserved(key) { + putLong(key, value) + } + + override fun putInt(key: String, value: Int) = assertKeyNotReserved(key) { + putInt(key, value) + } + + fun putStringSet(key: String?, value: Set?) = + _store.edit{ putStringSet(key, value) } + + override fun remove(key: String) = assertKeyNotReserved(key) { + remove(key) + } + + override fun contains(key: String): Boolean { + if (key == KEY_VERSION) return false + return _store.contains(key) + } + + override fun clearAll() { + val version = _store.getInt(KEY_VERSION, 0) + _store.edit { + clear() + putInt(KEY_VERSION, version) + } + } + + private fun onVersionUpdate(oldVersion: Int, version: Int, clearAllFlag: Boolean) { + if (clearAllFlag) { + clearAll() + } + } + + protected fun assertKeyNotReserved(key: Set, block: SharedPreferences.Editor.() -> Unit) { + key.forEach { require(it != KEY_VERSION) { "$it is a reserved key" } } + _store.edit { block(this) } + } + + protected fun assertKeyNotReserved(key: String, block: SharedPreferences.Editor.() -> Unit) { + require(key != KEY_VERSION) { "$key is a reserved key" } + _store.edit { block(this) } + } + + companion object { + @VisibleForTesting + const val KEY_VERSION: String = "__version__" + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.java b/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.java deleted file mode 100644 index d612880d9..000000000 --- a/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.java +++ /dev/null @@ -1,68 +0,0 @@ -package fr.free.nrw.commons.kvstore; - -import android.content.Context; - -import androidx.annotation.Nullable; - -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - -import java.lang.reflect.Type; -import java.util.HashMap; -import java.util.Map; - -public class JsonKvStore extends BasicKvStore { - private final Gson gson; - - public JsonKvStore(Context context, String storeName, Gson gson) { - super(context, storeName); - this.gson = gson; - } - - public JsonKvStore(Context context, String storeName, int version, Gson gson) { - super(context, storeName, version); - this.gson = gson; - } - - public JsonKvStore(Context context, String storeName, int version, boolean clearAllOnUpgrade, Gson gson) { - super(context, storeName, version, clearAllOnUpgrade); - this.gson = gson; - } - - public void putAllJsons(Map jsonMap) { - Map stringsMap = new HashMap<>(jsonMap.size()); - for (Map.Entry keyValuePair : jsonMap.entrySet()) { - String jsonString = gson.toJson(keyValuePair.getValue()); - stringsMap.put(keyValuePair.getKey(), jsonString); - } - putAllStrings(stringsMap); - } - - public void putJson(String key, T object) { - putString(key, gson.toJson(object)); - } - - public void putJsonWithTypeInfo(String key, T object, Type type) { - putString(key, gson.toJson(object, type)); - } - - @Nullable - public T getJson(String key, Class clazz) { - String jsonString = getString(key); - try { - return gson.fromJson(jsonString, clazz); - } catch (JsonSyntaxException e) { - return null; - } - } - - @Nullable - public T getJson(String key, Type type) { - String jsonString = getString(key); - try { - return gson.fromJson(jsonString, type); - } catch (JsonSyntaxException e) { - return null; - } - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt b/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt new file mode 100644 index 000000000..0f46222a4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt @@ -0,0 +1,52 @@ +package fr.free.nrw.commons.kvstore + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException + +class JsonKvStore : BasicKvStore { + val gson: Gson + + constructor(context: Context, storeName: String?, gson: Gson) : super(context, storeName) { + this.gson = gson + } + + constructor(context: Context, storeName: String?, version: Int, gson: Gson) : super( + context, storeName, version + ) { + this.gson = gson + } + + constructor( + context: Context, + storeName: String?, + version: Int, + clearAllOnUpgrade: Boolean, + gson: Gson + ) : super(context, storeName, version, clearAllOnUpgrade) { + this.gson = gson + } + + fun putJson(key: String, value: T) = assertKeyNotReserved(key) { + putString(key, gson.toJson(value)) + } + + @Deprecated( + message = "Migrate to newer Kotlin syntax", + replaceWith = ReplaceWith("getJson(key)") + ) + fun getJson(key: String, clazz: Class?): T? = try { + gson.fromJson(getString(key), clazz) + } catch (e: JsonSyntaxException) { + null + } + + // Later, when the calls are coming from Kotlin, this will allow us to + // drop the "clazz" parameter, and just pick up the type at the call site. + // The deprecation warning should help migration! + inline fun getJson(key: String): T? = try { + gson.fromJson(getString(key), T::class.java) + } catch (e: JsonSyntaxException) { + null + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.java b/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.java deleted file mode 100644 index 46d6d8f81..000000000 --- a/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.java +++ /dev/null @@ -1,35 +0,0 @@ -package fr.free.nrw.commons.kvstore; - -public interface KeyValueStore { - String getString(String key); - - boolean getBoolean(String key); - - long getLong(String key); - - int getInt(String key); - - String getString(String key, String defaultValue); - - boolean getBoolean(String key, boolean defaultValue); - - long getLong(String key, long defaultValue); - - int getInt(String key, int defaultValue); - - void putString(String key, String value); - - void putBoolean(String key, boolean value); - - void putLong(String key, long value); - - void putInt(String key, int value); - - boolean contains(String key); - - void remove(String key); - - void clearAll(); - - void clearAllWithVersion(); -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.kt b/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.kt new file mode 100644 index 000000000..6e19901cb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.kt @@ -0,0 +1,33 @@ +package fr.free.nrw.commons.kvstore + +interface KeyValueStore { + fun getString(key: String): String? + + fun getBoolean(key: String): Boolean + + fun getLong(key: String): Long + + fun getInt(key: String): Int + + fun getString(key: String, defaultValue: String?): String? + + fun getBoolean(key: String, defaultValue: Boolean): Boolean + + fun getLong(key: String, defaultValue: Long): Long + + fun getInt(key: String, defaultValue: Int): Int + + fun putString(key: String, value: String) + + fun putBoolean(key: String, value: Boolean) + + fun putLong(key: String, value: Long) + + fun putInt(key: String, value: Int) + + fun contains(key: String): Boolean + + fun remove(key: String) + + fun clearAll() +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt index f4b1f2625..87a710424 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt @@ -46,7 +46,7 @@ class SystemThemeUtils @Inject constructor( // Returns true if the device is in night mode or false otherwise fun isDeviceInNightMode(): Boolean { return getSystemDefaultThemeBool( - applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme()) + applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme())!! ) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/kvstore/BasicKvStoreTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/kvstore/BasicKvStoreTest.kt new file mode 100644 index 000000000..99fdf915b --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/kvstore/BasicKvStoreTest.kt @@ -0,0 +1,238 @@ +package fr.free.nrw.commons.kvstore + +import android.content.Context +import android.content.SharedPreferences +import com.nhaarman.mockitokotlin2.atLeast +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import fr.free.nrw.commons.kvstore.BasicKvStore.Companion.KEY_VERSION +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.mock + +class BasicKvStoreTest { + private val context = mock() + private val prefs = mock() + private val editor = mock() + private lateinit var store: BasicKvStore + + @Before + fun setUp() { + whenever(context.getSharedPreferences(anyString(), anyInt())).thenReturn(prefs) + whenever(prefs.edit()).thenReturn(editor) + store = BasicKvStore(context, "name") + } + + @Test + fun versionUpdate() { + whenever(prefs.getInt(KEY_VERSION, 0)).thenReturn(99) + BasicKvStore(context, "name", 100, true) + + // It should clear itself and automatically put the new version number + verify(prefs, atLeast(2)).edit() + verify(editor).clear() + verify(editor).putInt(KEY_VERSION, 100) + verify(editor, atLeast(2)).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun versionDowngradeNotAllowed() { + whenever(prefs.getInt(KEY_VERSION, 0)).thenReturn(100) + BasicKvStore(context, "name", 99, true) + } + + @Test + fun versionRedactedFromGetAll() { + val all = mutableMapOf("key" to "value", KEY_VERSION to 100) + whenever(prefs.all).thenReturn(all) + + val result = store.all + Assert.assertEquals(mapOf("key" to "value"), result) + } + + @Test + fun getAllHandlesNull() { + whenever(prefs.all).thenReturn(null) + Assert.assertNull(store.all) + } + + @Test + fun getAllHandlesEmpty() { + whenever(prefs.all).thenReturn(emptyMap()) + Assert.assertNull(store.all) + } + + @Test + fun getString() { + whenever(prefs.getString("key", null)).thenReturn("value") + Assert.assertEquals("value", store.getString("key")) + } + + @Test + fun getBoolean() { + whenever(prefs.getBoolean("key", false)).thenReturn(true) + Assert.assertTrue(store.getBoolean("key")) + } + + @Test + fun getLong() { + whenever(prefs.getLong("key", 0L)).thenReturn(100) + Assert.assertEquals(100L, store.getLong("key")) + } + + @Test + fun getInt() { + whenever(prefs.getInt("key", 0)).thenReturn(100) + Assert.assertEquals(100, store.getInt("key")) + } + + @Test + fun getStringWithDefault() { + whenever(prefs.getString("key", "junk")).thenReturn("value") + Assert.assertEquals("value", store.getString("key", "junk")) + } + + @Test + fun getBooleanWithDefault() { + whenever(prefs.getBoolean("key", true)).thenReturn(true) + Assert.assertTrue(store.getBoolean("key", true)) + } + + @Test + fun getLongWithDefault() { + whenever(prefs.getLong("key", 22L)).thenReturn(100) + Assert.assertEquals(100L, store.getLong("key", 22L)) + } + + @Test + fun getIntWithDefault() { + whenever(prefs.getInt("key", 22)).thenReturn(100) + Assert.assertEquals(100, store.getInt("key", 22)) + } + + @Test + fun putAllStrings() { + store.putAllStrings( + mapOf( + "one" to "fish", + "two" to "fish", + "red" to "fish", + "blue" to "fish" + ) + ) + + verify(prefs).edit() + verify(editor).putString("one", "fish") + verify(editor).putString("two", "fish") + verify(editor).putString("red", "fish") + verify(editor).putString("blue", "fish") + verify(editor).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun putAllStringsWithReservedKey() { + store.putAllStrings( + mapOf( + "this" to "that", + KEY_VERSION to "something" + ) + ) + } + + @Test + fun putString() { + store.putString("this" , "that") + + verify(prefs).edit() + verify(editor).putString("this", "that") + verify(editor).apply() + } + + @Test + fun putBoolean() { + store.putBoolean("this" , true) + + verify(prefs).edit() + verify(editor).putBoolean("this", true) + verify(editor).apply() + } + + @Test + fun putLong() { + store.putLong("this" , 123L) + + verify(prefs).edit() + verify(editor).putLong("this", 123L) + verify(editor).apply() + } + + @Test + fun putInt() { + store.putInt("this" , 16) + + verify(prefs).edit() + verify(editor).putInt("this", 16) + verify(editor).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun putStringWithReservedKey() { + store.putString(KEY_VERSION, "that") + } + + @Test(expected = IllegalArgumentException::class) + fun putBooleanWithReservedKey() { + store.putBoolean(KEY_VERSION, true) + } + + @Test(expected = IllegalArgumentException::class) + fun putLongWithReservedKey() { + store.putLong(KEY_VERSION, 33L) + } + + @Test(expected = IllegalArgumentException::class) + fun putIntWithReservedKey() { + store.putInt(KEY_VERSION, 12) + } + + @Test + fun testContains() { + whenever(prefs.contains("key")).thenReturn(true) + Assert.assertTrue(store.contains("key")) + } + + @Test + fun containsRedactsVersion() { + whenever(prefs.contains(KEY_VERSION)).thenReturn(true) + Assert.assertFalse(store.contains(KEY_VERSION)) + } + + @Test + fun remove() { + store.remove("key") + + verify(prefs).edit() + verify(editor).remove("key") + verify(editor).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun removeWithReservedKey() { + store.remove(KEY_VERSION) + } + + @Test + fun clearAllPreservesVersion() { + whenever(prefs.getInt(KEY_VERSION, 0)).thenReturn(99) + + store.clearAll() + + verify(prefs).edit() + verify(editor).clear() + verify(editor).putInt(KEY_VERSION, 99) + verify(editor).apply() + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/kvstore/JsonKvStoreTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/kvstore/JsonKvStoreTest.kt new file mode 100644 index 000000000..0a0bdfc47 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/kvstore/JsonKvStoreTest.kt @@ -0,0 +1,85 @@ +package fr.free.nrw.commons.kvstore + +import android.content.Context +import android.content.SharedPreferences +import com.google.gson.Gson +import com.nhaarman.mockitokotlin2.atLeast +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import fr.free.nrw.commons.kvstore.BasicKvStore.Companion.KEY_VERSION +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.mock + +class JsonKvStoreTest { + private val context = mock() + private val prefs = mock() + private val editor = mock() + + private val gson = Gson() + private val testData = Person(16, "Bob", true, Pet("Poodle", 2)) + private val expected = gson.toJson(testData) + + private lateinit var store: JsonKvStore + + @Before + fun setUp() { + whenever(context.getSharedPreferences(anyString(), anyInt())).thenReturn(prefs) + whenever(prefs.edit()).thenReturn(editor) + store = JsonKvStore(context, "name", gson) + } + + @Test + fun putJson() { + store.putJson("person", testData) + + verify(prefs).edit() + verify(editor).putString("person", expected) + verify(editor).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun putJsonWithReservedKey() { + store.putJson(KEY_VERSION, testData) + } + + @Test + fun getJson() { + whenever(prefs.getString("key", null)).thenReturn(expected) + + val result = store.getJson("key", Person::class.java) + + Assert.assertEquals(testData, result) + } + + @Test + fun getJsonInTheFuture() { + whenever(prefs.getString("key", null)).thenReturn(expected) + + val resultOne: Person? = store.getJson("key") + Assert.assertEquals(testData, resultOne) + + val resultTwo = store.getJson("key") + Assert.assertEquals(testData, resultTwo) + } + + @Test + fun getJsonHandlesMalformedJson() { + whenever(prefs.getString("key", null)).thenReturn("junk") + + val result = store.getJson("key", Person::class.java) + + Assert.assertNull(result) + } + + data class Person( + val age: Int, val name: String, val hasPets: Boolean, val pet: Pet? + ) + + data class Pet( + val breed: String, val age: Int + ) +} \ No newline at end of file