diff options
Diffstat (limited to 'mobile/android/android-components/components/feature/logins')
14 files changed, 612 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/feature/logins/README.md b/mobile/android/android-components/components/feature/logins/README.md new file mode 100644 index 0000000000..662567e3cb --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/README.md @@ -0,0 +1,19 @@ +# [Android Components](../../../README.md) > Feature > Logins + +Feature component with features related to logins. + +## Usage + +### Setting up the dependency + +Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)): + +```Groovy +implementation "org.mozilla.components:feature-logins:{latest-version}" +``` + +## License + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/ diff --git a/mobile/android/android-components/components/feature/logins/build.gradle b/mobile/android/android-components/components/feature/logins/build.gradle new file mode 100644 index 0000000000..fd4febbe32 --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/build.gradle @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'com.google.devtools.ksp' + +android { + defaultConfig { + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + ksp { + arg("room.schemaLocation", "$projectDir/schemas".toString()) + arg("room.generateKotlin", "true") + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + packagingOptions { + resources { + excludes += ['META-INF/proguard/androidx-annotations.pro'] + } + } + + sourceSets { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + } + + namespace 'mozilla.components.feature.logins' +} + +dependencies { + implementation project(':support-ktx') + implementation project(':support-base') + implementation project(':feature-prompts') + + implementation ComponentsDependencies.kotlin_coroutines + + implementation ComponentsDependencies.androidx_paging + implementation ComponentsDependencies.androidx_lifecycle_livedata + + implementation ComponentsDependencies.androidx_room_runtime + ksp ComponentsDependencies.androidx_room_compiler + + testImplementation project(':support-test') + + testImplementation ComponentsDependencies.androidx_test_core + testImplementation ComponentsDependencies.testing_junit + testImplementation ComponentsDependencies.testing_robolectric + testImplementation ComponentsDependencies.kotlin_coroutines + + androidTestImplementation project(':support-android-test') + + androidTestImplementation ComponentsDependencies.androidx_room_testing + androidTestImplementation ComponentsDependencies.androidx_arch_core_testing + androidTestImplementation ComponentsDependencies.androidx_test_core + androidTestImplementation ComponentsDependencies.androidx_test_runner + androidTestImplementation ComponentsDependencies.androidx_test_rules +} + +apply from: '../../../android-lint.gradle' +apply from: '../../../publish.gradle' +ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/mobile/android/android-components/components/feature/logins/proguard-rules.pro b/mobile/android/android-components/components/feature/logins/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/components/feature/logins/schemas/mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase/1.json b/mobile/android/android-components/components/feature/logins/schemas/mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase/1.json new file mode 100644 index 0000000000..e136e78b0b --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/schemas/mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase/1.json @@ -0,0 +1,40 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "bbf39f381a14e0e0a5544f54f0e1cedc", + "entities": [ + { + "tableName": "logins_exceptions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `origin` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bbf39f381a14e0e0a5544f54f0e1cedc')" + ] + } +} diff --git a/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/LoginExceptionStorageTest.kt b/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/LoginExceptionStorageTest.kt new file mode 100644 index 0000000000..749ee0b82f --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/LoginExceptionStorageTest.kt @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.logins + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.room.testing.MigrationTestHelper +import androidx.test.core.app.ApplicationProvider +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import mozilla.components.feature.logins.exceptions.LoginException +import mozilla.components.feature.logins.exceptions.LoginExceptionStorage +import mozilla.components.feature.logins.exceptions.adapter.LoginExceptionAdapter +import mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +@Suppress("LargeClass") +class LoginExceptionStorageTest { + private lateinit var context: Context + private lateinit var storage: LoginExceptionStorage + private lateinit var executor: ExecutorService + + @get:Rule + var instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + LoginExceptionDatabase::class.java, + ) + + @Before + fun setUp() { + executor = Executors.newSingleThreadExecutor() + + context = ApplicationProvider.getApplicationContext() + val database = + Room.inMemoryDatabaseBuilder(context, LoginExceptionDatabase::class.java).build() + + storage = + LoginExceptionStorage( + context, + ) + storage.database = lazy { database } + } + + @After + fun tearDown() { + executor.shutdown() + } + + @Test + fun testAddingExceptions() { + storage.addLoginException("mozilla.org") + storage.addLoginException("firefox.com") + + val exceptions = getAllExceptions() + + assertEquals(2, exceptions.size) + + assertEquals("mozilla.org", exceptions[0].origin) + assertEquals("firefox.com", exceptions[1].origin) + } + + @Test + fun testRemovingExceptions() { + storage.addLoginException("mozilla.org") + storage.addLoginException("firefox.com") + + getAllExceptions().let { exceptions -> + assertEquals(2, exceptions.size) + storage.removeLoginException(exceptions[0]) + } + + getAllExceptions().let { exceptions -> + assertEquals(1, exceptions.size) + assertEquals("firefox.com", exceptions[0].origin) + } + } + + @Test + fun testGettingExceptions() = runBlocking { + storage.addLoginException("mozilla.org") + storage.addLoginException("firefox.com") + + val exceptions = storage.getLoginExceptions().first() + + assertNotNull(exceptions) + assertEquals(2, exceptions.size) + + with(exceptions[0]) { + assertEquals("mozilla.org", origin) + } + + with(exceptions[1]) { + assertEquals("firefox.com", origin) + } + } + + @Test + fun testGettingExceptionsByOrigin() = runBlocking { + storage.addLoginException("mozilla.org") + storage.addLoginException("firefox.com") + + val exception = storage.findExceptionByOrigin("mozilla.org") + + assertNotNull(exception) + assertEquals("mozilla.org", exception!!.origin) + } + + @Test + fun testGettingNoExceptionsByOrigin() = runBlocking { + storage.addLoginException("mozilla.org") + storage.addLoginException("firefox.com") + + val exception = storage.findExceptionByOrigin("testsite.org") + + assertNull(exception) + } + + private fun getAllExceptions(): List<LoginException> { + return storage.database.value.loginExceptionDao().getLoginExceptionsList() + .map { loginExceptionEntity -> + LoginExceptionAdapter(loginExceptionEntity) + } + } +} diff --git a/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDaoTest.kt b/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDaoTest.kt new file mode 100644 index 0000000000..1354a7f15d --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDaoTest.kt @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.logins.exceptions.db + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class LoginExceptionDaoTest { + private val context: Context + get() = ApplicationProvider.getApplicationContext() + + private lateinit var database: LoginExceptionDatabase + private lateinit var loginExceptionDao: LoginExceptionDao + private lateinit var executor: ExecutorService + + @get:Rule + var instantTaskExecutorRule = InstantTaskExecutorRule() + + @Before + fun setUp() { + database = Room.inMemoryDatabaseBuilder(context, LoginExceptionDatabase::class.java).build() + loginExceptionDao = database.loginExceptionDao() + executor = Executors.newSingleThreadExecutor() + } + + @After + fun tearDown() { + database.close() + executor.shutdown() + } + + @Test + fun testAddingLoginException() { + val exception = LoginExceptionEntity( + origin = "mozilla.org", + ).also { + it.id = loginExceptionDao.insertLoginException(it) + } + + val loginExceptionsList = loginExceptionDao.getLoginExceptionsList() + + assertEquals(1, loginExceptionsList.size) + assertEquals(exception, loginExceptionsList[0]) + } + + @Test + fun testRemovingLoginException() { + val exception1 = LoginExceptionEntity( + origin = "mozilla.org", + ).also { + it.id = loginExceptionDao.insertLoginException(it) + } + + val exception2 = LoginExceptionEntity( + origin = "firefox.com", + ).also { + it.id = loginExceptionDao.insertLoginException(it) + } + + loginExceptionDao.deleteLoginException(exception1) + + val loginExceptionsList = loginExceptionDao.getLoginExceptionsList() + + assertEquals(1, loginExceptionsList.size) + assertEquals(exception2, loginExceptionsList[0]) + } +} diff --git a/mobile/android/android-components/components/feature/logins/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/logins/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e16cda1d34 --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<manifest /> diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginException.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginException.kt new file mode 100644 index 0000000000..bd4c348987 --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginException.kt @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.logins.exceptions + +/** + * A login exception. + */ +interface LoginException { + /** + * Unique ID of this login exception. + */ + val id: Long + + /** + * The origin of the login exception site. + */ + val origin: String +} diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginExceptionStorage.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginExceptionStorage.kt new file mode 100644 index 0000000000..4b86ebb48b --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginExceptionStorage.kt @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.logins.exceptions + +import android.content.Context +import androidx.paging.DataSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import mozilla.components.feature.logins.exceptions.adapter.LoginExceptionAdapter +import mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase +import mozilla.components.feature.logins.exceptions.db.LoginExceptionEntity +import mozilla.components.feature.prompts.login.LoginExceptions + +/** + * A storage implementation for organizing login exceptions. + */ +class LoginExceptionStorage( + context: Context, +) : LoginExceptions { + internal var database: Lazy<LoginExceptionDatabase> = + lazy { LoginExceptionDatabase.get(context) } + + /** + * Adds a new [LoginException]. + * + * @param origin The origin. + */ + override fun addLoginException(origin: String) { + LoginExceptionEntity( + origin = origin, + ).also { entity -> + entity.id = database.value.loginExceptionDao().insertLoginException(entity) + } + } + + /** + * Returns a [Flow] list of all the [LoginException] instances. + */ + fun getLoginExceptions(): Flow<List<LoginException>> { + return database.value.loginExceptionDao().getLoginExceptions().map { list -> + list.map { entity -> LoginExceptionAdapter(entity) } + } + } + + /** + * Returns all [LoginException]s as a [DataSource.Factory]. + */ + fun getLoginExceptionsPaged(): DataSource.Factory<Int, LoginException> = database.value + .loginExceptionDao() + .getLoginExceptionsPaged() + .map { entity -> LoginExceptionAdapter(entity) } + + /** + * Removes the given [LoginException]. + */ + fun removeLoginException(site: LoginException) { + val exceptionEntity = (site as LoginExceptionAdapter).entity + database.value.loginExceptionDao().deleteLoginException(exceptionEntity) + } + + override fun isLoginExceptionByOrigin(origin: String): Boolean { + return findExceptionByOrigin(origin) != null + } + + /** + * Finds a [LoginException] by origin. + */ + fun findExceptionByOrigin(origin: String): LoginException? { + val exception = database.value.loginExceptionDao().findExceptionByOrigin(origin) + return exception?.let { + LoginExceptionAdapter( + it, + ) + } + } + + /** + * Removes all [LoginException]s. + */ + fun deleteAllLoginExceptions() { + database.value.loginExceptionDao().deleteAllLoginExceptions() + } +} diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/adapter/LoginExceptionAdapter.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/adapter/LoginExceptionAdapter.kt new file mode 100644 index 0000000000..9c0a35a8aa --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/adapter/LoginExceptionAdapter.kt @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.logins.exceptions.adapter + +import mozilla.components.feature.logins.exceptions.LoginException +import mozilla.components.feature.logins.exceptions.db.LoginExceptionEntity + +internal class LoginExceptionAdapter( + internal val entity: LoginExceptionEntity, +) : LoginException { + override val id: Long + get() = entity.id!! + + override val origin: String + get() = entity.origin + + override fun equals(other: Any?): Boolean { + if (other !is LoginExceptionAdapter) { + return false + } + + return entity == other.entity + } + + override fun hashCode(): Int { + return entity.hashCode() + } +} diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDao.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDao.kt new file mode 100644 index 0000000000..e7bac42abc --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDao.kt @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.logins.exceptions.db + +import androidx.paging.DataSource +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Transaction +import kotlinx.coroutines.flow.Flow + +/** + * Internal DAO for accessing [LoginExceptionEntity] instances. + */ +@Dao +internal interface LoginExceptionDao { + @Insert + fun insertLoginException(exception: LoginExceptionEntity): Long + + @Delete + fun deleteLoginException(exception: LoginExceptionEntity) + + @Transaction + @Query("SELECT * FROM logins_exceptions") + fun getLoginExceptions(): Flow<List<LoginExceptionEntity>> + + @Transaction + @Query("SELECT * FROM logins_exceptions") + fun getLoginExceptionsList(): List<LoginExceptionEntity> + + @Query("DELETE FROM logins_exceptions") + fun deleteAllLoginExceptions() + + @Query("SELECT * FROM logins_exceptions WHERE origin = :origin") + fun findExceptionByOrigin(origin: String): LoginExceptionEntity? + + @Transaction + @Query("SELECT * FROM logins_exceptions") + fun getLoginExceptionsPaged(): DataSource.Factory<Int, LoginExceptionEntity> +} diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDatabase.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDatabase.kt new file mode 100644 index 0000000000..e1125aa44f --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDatabase.kt @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.logins.exceptions.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +/** + * Internal database for storing login exceptions. + */ +@Database(entities = [LoginExceptionEntity::class], version = 1) +internal abstract class LoginExceptionDatabase : RoomDatabase() { + abstract fun loginExceptionDao(): LoginExceptionDao + + companion object { + @Volatile + private var instance: LoginExceptionDatabase? = null + + @Synchronized + fun get(context: Context): LoginExceptionDatabase { + instance?.let { return it } + + return Room.databaseBuilder( + context, + LoginExceptionDatabase::class.java, + "login_exceptions", + ).build().also { + instance = it + } + } + } +} diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionEntity.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionEntity.kt new file mode 100644 index 0000000000..fee109f835 --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionEntity.kt @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.logins.exceptions.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Internal entity representing a login exception. + */ +@Entity(tableName = "logins_exceptions") +internal data class LoginExceptionEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") + var id: Long? = null, + + @ColumnInfo(name = "origin") + var origin: String, +) diff --git a/mobile/android/android-components/components/feature/logins/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/logins/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..cf1c399ea8 --- /dev/null +++ b/mobile/android/android-components/components/feature/logins/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1,2 @@ +mock-maker-inline +// This allows mocking final classes (classes are final by default in Kotlin) |