mirror of https://github.com/tuskyapp/Tusky.git
Browse Source
- use `runTest` instead of `runBlocking`, where possible - run all Robolectric tests on Api 34 (where we have most users) - some new testcase for `TimestampUtilsTest` - move our only instrumented Android Test, `MigrationsTest`, to unit test so it runs in CI and expand it to test all migrations - upgrade Robolectric - removed truth and espresso as they are no longer neededpull/4784/head
29 changed files with 554 additions and 360 deletions
@ -1,66 +0,0 @@
|
||||
package com.keylesspalace.tusky |
||||
|
||||
import androidx.room.testing.MigrationTestHelper |
||||
import androidx.test.ext.junit.runners.AndroidJUnit4 |
||||
import androidx.test.platform.app.InstrumentationRegistry |
||||
import com.keylesspalace.tusky.db.AppDatabase |
||||
import org.junit.Assert.assertEquals |
||||
import org.junit.Rule |
||||
import org.junit.Test |
||||
import org.junit.runner.RunWith |
||||
|
||||
const val TEST_DB = "migration_test" |
||||
|
||||
@RunWith(AndroidJUnit4::class) |
||||
class MigrationsTest { |
||||
|
||||
@JvmField |
||||
@Rule |
||||
var helper: MigrationTestHelper = MigrationTestHelper( |
||||
InstrumentationRegistry.getInstrumentation(), |
||||
AppDatabase::class.java |
||||
) |
||||
|
||||
@Test |
||||
fun migrateTo11() { |
||||
val db = helper.createDatabase(TEST_DB, 10) |
||||
|
||||
val id = 1 |
||||
val domain = "domain.site" |
||||
val token = "token" |
||||
val active = true |
||||
val accountId = "accountId" |
||||
val username = "username" |
||||
val values = arrayOf( |
||||
id, domain, token, active, accountId, username, "Display Name", |
||||
"https://picture.url", true, true, true, true, true, true, true, |
||||
true, "1000", "[]", "[{\"shortcode\": \"emoji\", \"url\": \"yes\"}]", 0, false, |
||||
false, true |
||||
) |
||||
|
||||
db.execSQL( |
||||
"INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," + |
||||
"`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`," + |
||||
"`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," + |
||||
"`notificationsFavorited`,`notificationSound`,`notificationVibration`," + |
||||
"`notificationLight`,`lastNotificationId`,`activeNotifications`,`emojis`," + |
||||
"`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," + |
||||
"`mediaPreviewEnabled`) " + |
||||
"VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", |
||||
values |
||||
) |
||||
|
||||
db.close() |
||||
|
||||
val newDb = helper.runMigrationsAndValidate(TEST_DB, 11, true, AppDatabase.MIGRATION_10_11) |
||||
|
||||
val cursor = newDb.query("SELECT * FROM AccountEntity") |
||||
cursor.moveToFirst() |
||||
assertEquals(id, cursor.getInt(0)) |
||||
assertEquals(domain, cursor.getString(1)) |
||||
assertEquals(token, cursor.getString(2)) |
||||
assertEquals(active, cursor.getInt(3) != 0) |
||||
assertEquals(accountId, cursor.getString(4)) |
||||
assertEquals(username, cursor.getString(5)) |
||||
} |
||||
} |
||||
@ -1,49 +0,0 @@
|
||||
package android.text |
||||
|
||||
// Used for stubbing Android implementation without slow & buggy Robolectric things |
||||
@Suppress("unused") |
||||
class SpannableString(private val text: CharSequence) : Spannable { |
||||
|
||||
override fun setSpan(what: Any?, start: Int, end: Int, flags: Int) { |
||||
throw NotImplementedError() |
||||
} |
||||
|
||||
override fun <T : Any?> getSpans(start: Int, end: Int, type: Class<T>?): Array<T> { |
||||
throw NotImplementedError() |
||||
} |
||||
|
||||
override fun removeSpan(what: Any?) { |
||||
throw NotImplementedError() |
||||
} |
||||
|
||||
override fun toString(): String { |
||||
return "FakeSpannableString[text=$text]" |
||||
} |
||||
|
||||
override val length: Int |
||||
get() = text.length |
||||
|
||||
override fun nextSpanTransition(start: Int, limit: Int, type: Class<*>?): Int { |
||||
throw NotImplementedError() |
||||
} |
||||
|
||||
override fun getSpanEnd(tag: Any?): Int { |
||||
throw NotImplementedError() |
||||
} |
||||
|
||||
override fun getSpanFlags(tag: Any?): Int { |
||||
throw NotImplementedError() |
||||
} |
||||
|
||||
override fun get(index: Int): Char { |
||||
throw NotImplementedError() |
||||
} |
||||
|
||||
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { |
||||
throw NotImplementedError() |
||||
} |
||||
|
||||
override fun getSpanStart(tag: Any?): Int { |
||||
throw NotImplementedError() |
||||
} |
||||
} |
||||
@ -0,0 +1,87 @@
|
||||
package com.keylesspalace.tusky.db |
||||
|
||||
import androidx.room.testing.MigrationTestHelper |
||||
import androidx.test.ext.junit.runners.AndroidJUnit4 |
||||
import androidx.test.platform.app.InstrumentationRegistry |
||||
import com.keylesspalace.tusky.di.StorageModule |
||||
import com.keylesspalace.tusky.entity.Emoji |
||||
import com.squareup.moshi.Moshi |
||||
import com.squareup.moshi.Types |
||||
import org.junit.Assert.assertEquals |
||||
import org.junit.Rule |
||||
import org.junit.Test |
||||
import org.junit.runner.RunWith |
||||
import org.robolectric.annotation.Config |
||||
|
||||
@Config(sdk = [34]) |
||||
@RunWith(AndroidJUnit4::class) |
||||
class MigrationsTest { |
||||
|
||||
@get:Rule |
||||
val migrationHelper = MigrationTestHelper( |
||||
InstrumentationRegistry.getInstrumentation(), |
||||
AppDatabase::class.java |
||||
) |
||||
|
||||
@Test |
||||
fun testMigrations() { |
||||
/** the db name must match the one in [StorageModule.providesDatabase] */ |
||||
val db = migrationHelper.createDatabase("tuskyDB", 10) |
||||
val moshi = Moshi.Builder().build() |
||||
|
||||
val id = 1L |
||||
val domain = "domain.site" |
||||
val token = "token" |
||||
val active = true |
||||
val accountId = "accountId" |
||||
val username = "username" |
||||
val emoji = moshi.adapter<List<Emoji>>(Types.newParameterizedType(List::class.java, Emoji::class.java), emptySet()).toJson( |
||||
listOf( |
||||
Emoji( |
||||
shortcode = "testemoji", |
||||
url = "https://some.url", |
||||
staticUrl = "https://some.url", |
||||
visibleInPicker = true, |
||||
category = null |
||||
) |
||||
) |
||||
) |
||||
val values = arrayOf( |
||||
id, domain, token, active, accountId, username, "Display Name", |
||||
"https://picture.url", true, true, true, true, true, true, true, |
||||
true, "1000", "[]", emoji, 0, false, |
||||
false, true |
||||
) |
||||
|
||||
db.execSQL( |
||||
"INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," + |
||||
"`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`," + |
||||
"`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," + |
||||
"`notificationsFavorited`,`notificationSound`,`notificationVibration`," + |
||||
"`notificationLight`,`lastNotificationId`,`activeNotifications`,`emojis`," + |
||||
"`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," + |
||||
"`mediaPreviewEnabled`) " + |
||||
"VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", |
||||
values |
||||
) |
||||
|
||||
db.close() |
||||
|
||||
// Room will run all migrations and validate the scheme afterwards |
||||
val roomDb = StorageModule.providesDatabase( |
||||
InstrumentationRegistry.getInstrumentation().context, |
||||
Converters(moshi) |
||||
) |
||||
|
||||
val account = roomDb.accountDao().loadAll().first() |
||||
|
||||
roomDb.close() |
||||
|
||||
assertEquals(id, account.id) |
||||
assertEquals(domain, account.domain) |
||||
assertEquals(token, account.accessToken) |
||||
assertEquals(active, account.isActive) |
||||
assertEquals(accountId, account.accountId) |
||||
assertEquals(username, account.username) |
||||
} |
||||
} |
||||
@ -1,46 +1,48 @@
|
||||
package com.keylesspalace.tusky.entity |
||||
|
||||
import com.keylesspalace.tusky.settings.ProxyConfiguration |
||||
import org.junit.Assert |
||||
import org.junit.Assert.assertFalse |
||||
import org.junit.Assert.assertNull |
||||
import org.junit.Assert.assertTrue |
||||
import org.junit.Test |
||||
|
||||
class ProxyConfigurationTest { |
||||
@Test |
||||
fun `serialized non-int is not valid proxy port`() { |
||||
Assert.assertFalse(ProxyConfiguration.isValidProxyPort("should fail")) |
||||
Assert.assertFalse(ProxyConfiguration.isValidProxyPort("1.5")) |
||||
assertFalse(ProxyConfiguration.isValidProxyPort("should fail")) |
||||
assertFalse(ProxyConfiguration.isValidProxyPort("1.5")) |
||||
} |
||||
|
||||
@Test |
||||
fun `number outside port range is not valid`() { |
||||
Assert.assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MIN_PROXY_PORT - 1}")) |
||||
Assert.assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MAX_PROXY_PORT + 1}")) |
||||
assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MIN_PROXY_PORT - 1}")) |
||||
assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MAX_PROXY_PORT + 1}")) |
||||
} |
||||
|
||||
@Test |
||||
fun `number in port range, inclusive of min and max, is valid`() { |
||||
Assert.assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MIN_PROXY_PORT)) |
||||
Assert.assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MAX_PROXY_PORT)) |
||||
Assert.assertTrue(ProxyConfiguration.isValidProxyPort((ProxyConfiguration.MIN_PROXY_PORT + ProxyConfiguration.MAX_PROXY_PORT) / 2)) |
||||
assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MIN_PROXY_PORT)) |
||||
assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MAX_PROXY_PORT)) |
||||
assertTrue(ProxyConfiguration.isValidProxyPort((ProxyConfiguration.MIN_PROXY_PORT + ProxyConfiguration.MAX_PROXY_PORT) / 2)) |
||||
} |
||||
|
||||
@Test |
||||
fun `create with invalid port yields null`() { |
||||
Assert.assertNull(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT - 1)) |
||||
assertNull(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT - 1)) |
||||
} |
||||
|
||||
@Test |
||||
fun `create with invalid hostname yields null`() { |
||||
Assert.assertNull(ProxyConfiguration.create(".", ProxyConfiguration.MIN_PROXY_PORT)) |
||||
assertNull(ProxyConfiguration.create(".", ProxyConfiguration.MIN_PROXY_PORT)) |
||||
} |
||||
|
||||
@Test |
||||
fun `create with valid hostname and port yields the config object`() { |
||||
Assert.assertTrue(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration) |
||||
assertTrue(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration) |
||||
} |
||||
|
||||
@Test |
||||
fun `unicode hostname allowed`() { |
||||
Assert.assertTrue(ProxyConfiguration.create("federação.social", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration) |
||||
assertTrue(ProxyConfiguration.create("federação.social", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration) |
||||
} |
||||
} |
||||
|
||||
@ -1,29 +1,78 @@
|
||||
package com.keylesspalace.tusky.util |
||||
|
||||
import android.content.Context |
||||
import androidx.test.ext.junit.runners.AndroidJUnit4 |
||||
import com.keylesspalace.tusky.R |
||||
import androidx.test.platform.app.InstrumentationRegistry |
||||
import java.util.Locale |
||||
import kotlin.time.Duration.Companion.days |
||||
import kotlin.time.Duration.Companion.hours |
||||
import kotlin.time.Duration.Companion.minutes |
||||
import kotlin.time.Duration.Companion.seconds |
||||
import org.junit.AfterClass |
||||
import org.junit.Assert.assertEquals |
||||
import org.junit.BeforeClass |
||||
import org.junit.Test |
||||
import org.junit.runner.RunWith |
||||
import org.mockito.kotlin.doReturn |
||||
import org.mockito.kotlin.mock |
||||
import org.robolectric.annotation.Config |
||||
|
||||
private const val STATUS_CREATED_AT_NOW = "test" |
||||
|
||||
@Config(sdk = [28]) |
||||
@Config(sdk = [34]) |
||||
@RunWith(AndroidJUnit4::class) |
||||
class TimestampUtilsTest { |
||||
private val ctx: Context = mock { |
||||
on { getString(R.string.status_created_at_now) } doReturn STATUS_CREATED_AT_NOW |
||||
|
||||
companion object { |
||||
private lateinit var locale: Locale |
||||
|
||||
@BeforeClass |
||||
@JvmStatic |
||||
fun beforeClass() { |
||||
locale = Locale.getDefault() |
||||
Locale.setDefault(Locale.ENGLISH) |
||||
} |
||||
|
||||
@AfterClass |
||||
@JvmStatic |
||||
fun afterClass() { |
||||
Locale.setDefault(locale) |
||||
} |
||||
} |
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext |
||||
|
||||
@Test |
||||
fun `should return 'now' for small timespans`() { |
||||
assertEquals("now", getRelativeTimeSpanString(context, 0, 300)) |
||||
assertEquals("now", getRelativeTimeSpanString(context, 300, 0)) |
||||
assertEquals("now", getRelativeTimeSpanString(context, 501, 0)) |
||||
assertEquals("now", getRelativeTimeSpanString(context, 0, 999)) |
||||
} |
||||
|
||||
@Test |
||||
fun `should return 'in --' when then is after now`() { |
||||
assertEquals("in 49s", getRelativeTimeSpanString(context, 49.seconds.inWholeMilliseconds, 0)) |
||||
assertEquals("in 34m", getRelativeTimeSpanString(context, 37.minutes.inWholeMilliseconds, 3.minutes.inWholeMilliseconds)) |
||||
assertEquals("in 7h", getRelativeTimeSpanString(context, 10.hours.inWholeMilliseconds, 3.hours.inWholeMilliseconds)) |
||||
assertEquals("in 10d", getRelativeTimeSpanString(context, 10.days.inWholeMilliseconds, 0)) |
||||
assertEquals("in 4y", getRelativeTimeSpanString(context, 800.days.inWholeMilliseconds + (4 * 365).days.inWholeMilliseconds, 800.days.inWholeMilliseconds)) |
||||
} |
||||
|
||||
@Test |
||||
fun `should return correct timespans`() { |
||||
assertEquals("49s", getRelativeTimeSpanString(context, 0, 49.seconds.inWholeMilliseconds)) |
||||
assertEquals("34m", getRelativeTimeSpanString(context, 3.minutes.inWholeMilliseconds, 37.minutes.inWholeMilliseconds)) |
||||
assertEquals("7h", getRelativeTimeSpanString(context, 3.hours.inWholeMilliseconds, 10.hours.inWholeMilliseconds)) |
||||
assertEquals("10d", getRelativeTimeSpanString(context, 0, 10.days.inWholeMilliseconds)) |
||||
assertEquals("4y", getRelativeTimeSpanString(context, 800.days.inWholeMilliseconds, 800.days.inWholeMilliseconds + (4 * 365).days.inWholeMilliseconds)) |
||||
} |
||||
|
||||
@Test |
||||
fun shouldShowNowForSmallTimeSpans() { |
||||
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 300)) |
||||
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 300, 0)) |
||||
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 501, 0)) |
||||
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 999)) |
||||
fun `should return correct poll duration`() { |
||||
assertEquals("1 second left", formatPollDuration(context, 1.seconds.inWholeMilliseconds, 0)) |
||||
assertEquals("49 seconds left", formatPollDuration(context, 49.seconds.inWholeMilliseconds, 0)) |
||||
assertEquals("1 minute left", formatPollDuration(context, 37.minutes.inWholeMilliseconds, 36.minutes.inWholeMilliseconds)) |
||||
assertEquals("34 minutes left", formatPollDuration(context, 37.minutes.inWholeMilliseconds, 3.minutes.inWholeMilliseconds)) |
||||
assertEquals("1 hour left", formatPollDuration(context, 10.hours.inWholeMilliseconds, 9.hours.inWholeMilliseconds)) |
||||
assertEquals("7 hours left", formatPollDuration(context, 10.hours.inWholeMilliseconds, 3.hours.inWholeMilliseconds)) |
||||
assertEquals("1 day left", formatPollDuration(context, 1.days.inWholeMilliseconds, 0)) |
||||
assertEquals("10 days left", formatPollDuration(context, 10.days.inWholeMilliseconds, 0)) |
||||
assertEquals("1460 days left", formatPollDuration(context, 800.days.inWholeMilliseconds + (4 * 365).days.inWholeMilliseconds, 800.days.inWholeMilliseconds)) |
||||
} |
||||
} |
||||
|
||||
Loading…
Reference in new issue