feat(update): add update detection and notification on status screen (WB-008)
Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
parent
02cbf20946
commit
613ceb01b7
10 changed files with 557 additions and 8 deletions
|
|
@ -0,0 +1,67 @@
|
|||
package com.rukira.wowbackup.ui.status
|
||||
|
||||
import com.rukira.wowbackup.update.UpdateInfo
|
||||
import com.rukira.wowbackup.update.UpdateService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class StatusViewModelTest {
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testViewModelReceivesUpdateInfo() = runTest(testDispatcher) {
|
||||
val mockService = object : UpdateService {
|
||||
override suspend fun checkForUpdate(currentVersion: String): UpdateInfo {
|
||||
return UpdateInfo(
|
||||
latestVersion = "1.0.2",
|
||||
releaseUrl = "https://git.asarius.site/rukira/wow-backup/releases/tag/v1.0.2",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val viewModel = StatusViewModel(updateService = mockService)
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = viewModel.state.first { it.updateInfo != null }
|
||||
assertNotNull(state.updateInfo)
|
||||
assertEquals("1.0.2", state.updateInfo!!.latestVersion)
|
||||
assertEquals("https://git.asarius.site/rukira/wow-backup/releases/tag/v1.0.2", state.updateInfo!!.releaseUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testViewModelHandlesNoUpdate() = runTest(testDispatcher) {
|
||||
val mockService = object : UpdateService {
|
||||
override suspend fun checkForUpdate(currentVersion: String): UpdateInfo? = null
|
||||
}
|
||||
|
||||
val viewModel = StatusViewModel(updateService = mockService)
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = viewModel.state.value
|
||||
assertNull(state.updateInfo)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.rukira.wowbackup.update
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class SemVerTest {
|
||||
|
||||
@Test
|
||||
fun testParseStandardVersions() {
|
||||
val v1 = SemVer.parseOrNull("1.0.0")
|
||||
assertNotNull(v1)
|
||||
assertEquals(1, v1.major)
|
||||
assertEquals(0, v1.minor)
|
||||
assertEquals(0, v1.patch)
|
||||
assertNull(v1.preRelease)
|
||||
|
||||
val v2 = SemVer.parseOrNull("v1.2.3")
|
||||
assertNotNull(v2)
|
||||
assertEquals(1, v2.major)
|
||||
assertEquals(2, v2.minor)
|
||||
assertEquals(3, v2.patch)
|
||||
assertNull(v2.preRelease)
|
||||
|
||||
val v3 = SemVer.parseOrNull("V2.0")
|
||||
assertNotNull(v3)
|
||||
assertEquals(2, v3.major)
|
||||
assertEquals(0, v3.minor)
|
||||
assertEquals(0, v3.patch)
|
||||
|
||||
val v4 = SemVer.parseOrNull("0.0.1")
|
||||
assertNotNull(v4)
|
||||
assertEquals(0, v4.major)
|
||||
assertEquals(0, v4.minor)
|
||||
assertEquals(1, v4.patch)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParsePreReleaseVersions() {
|
||||
val v = SemVer.parseOrNull("1.0.0-beta.1")
|
||||
assertNotNull(v)
|
||||
assertEquals(1, v.major)
|
||||
assertEquals(0, v.minor)
|
||||
assertEquals(0, v.patch)
|
||||
assertEquals("beta.1", v.preRelease)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseInvalidVersions() {
|
||||
assertNull(SemVer.parseOrNull(null))
|
||||
assertNull(SemVer.parseOrNull(""))
|
||||
assertNull(SemVer.parseOrNull(" "))
|
||||
assertNull(SemVer.parseOrNull("invalid"))
|
||||
assertNull(SemVer.parseOrNull("1.a.3"))
|
||||
assertNull(SemVer.parseOrNull("1.2.3.4"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testVersionComparison() {
|
||||
val v001 = SemVer.parseOrNull("0.0.1")!!
|
||||
val v100 = SemVer.parseOrNull("1.0.0")!!
|
||||
val v101 = SemVer.parseOrNull("1.0.1")!!
|
||||
val v102 = SemVer.parseOrNull("v1.0.2")!!
|
||||
val v110 = SemVer.parseOrNull("1.1.0")!!
|
||||
val v200 = SemVer.parseOrNull("2.0.0")!!
|
||||
|
||||
assertTrue(v100.isNewerThan(v001))
|
||||
assertTrue(v101.isNewerThan(v100))
|
||||
assertTrue(v102.isNewerThan(v101))
|
||||
assertTrue(v110.isNewerThan(v102))
|
||||
assertTrue(v200.isNewerThan(v110))
|
||||
|
||||
assertFalse(v101.isNewerThan(v102))
|
||||
assertFalse(v102.isNewerThan(SemVer.parseOrNull("1.0.2")!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPreReleaseComparison() {
|
||||
val release = SemVer.parseOrNull("1.0.0")!!
|
||||
val preRelease = SemVer.parseOrNull("1.0.0-alpha")!!
|
||||
val preReleaseBeta = SemVer.parseOrNull("1.0.0-beta")!!
|
||||
|
||||
assertTrue(release.isNewerThan(preRelease))
|
||||
assertFalse(preRelease.isNewerThan(release))
|
||||
assertTrue(preReleaseBeta.isNewerThan(preRelease))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToString() {
|
||||
assertEquals("1.0.0", SemVer.parseOrNull("1.0.0")?.toString())
|
||||
assertEquals("1.2.0", SemVer.parseOrNull("v1.2")?.toString())
|
||||
assertEquals("1.0.0-rc1", SemVer.parseOrNull("1.0.0-rc1")?.toString())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
package com.rukira.wowbackup.update
|
||||
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.net.InetSocketAddress
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class UpdateCheckerTest {
|
||||
|
||||
private lateinit var server: HttpServer
|
||||
private var serverPort: Int = 0
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
server = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0)
|
||||
serverPort = server.address.port
|
||||
server.start()
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
server.stop(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpdateAvailableWhenRemoteIsNewer() = runBlocking {
|
||||
val jsonResponse = """
|
||||
{
|
||||
"id": 5,
|
||||
"tag_name": "v1.0.2",
|
||||
"name": "WoW Backup v1.0.2",
|
||||
"html_url": "https://git.asarius.site/rukira/wow-backup/releases/tag/v1.0.2"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
server.createContext("/releases/latest") { exchange ->
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
val bytes = jsonResponse.toByteArray()
|
||||
exchange.sendResponseHeaders(200, bytes.size.toLong())
|
||||
exchange.responseBody.write(bytes)
|
||||
exchange.close()
|
||||
}
|
||||
|
||||
val checker = UpdateChecker(
|
||||
apiUrl = "http://127.0.0.1:$serverPort/releases/latest",
|
||||
fallbackUrl = "https://git.asarius.site/rukira/wow-backup/releases",
|
||||
)
|
||||
|
||||
val result = checker.checkForUpdate(currentVersion = "1.0.1")
|
||||
assertNotNull(result)
|
||||
assertEquals("1.0.2", result.latestVersion)
|
||||
assertEquals("https://git.asarius.site/rukira/wow-backup/releases/tag/v1.0.2", result.releaseUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoUpdateWhenUpToDate() = runBlocking {
|
||||
val jsonResponse = """
|
||||
{
|
||||
"id": 5,
|
||||
"tag_name": "v1.0.2",
|
||||
"name": "WoW Backup v1.0.2",
|
||||
"html_url": "https://git.asarius.site/rukira/wow-backup/releases/tag/v1.0.2"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
server.createContext("/releases/latest") { exchange ->
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
val bytes = jsonResponse.toByteArray()
|
||||
exchange.sendResponseHeaders(200, bytes.size.toLong())
|
||||
exchange.responseBody.write(bytes)
|
||||
exchange.close()
|
||||
}
|
||||
|
||||
val checker = UpdateChecker(
|
||||
apiUrl = "http://127.0.0.1:$serverPort/releases/latest",
|
||||
)
|
||||
|
||||
val result = checker.checkForUpdate(currentVersion = "1.0.2")
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoUpdateWhenRunningNewerVersion() = runBlocking {
|
||||
val jsonResponse = """
|
||||
{
|
||||
"id": 5,
|
||||
"tag_name": "v1.0.2",
|
||||
"name": "WoW Backup v1.0.2",
|
||||
"html_url": "https://git.asarius.site/rukira/wow-backup/releases/tag/v1.0.2"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
server.createContext("/releases/latest") { exchange ->
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
val bytes = jsonResponse.toByteArray()
|
||||
exchange.sendResponseHeaders(200, bytes.size.toLong())
|
||||
exchange.responseBody.write(bytes)
|
||||
exchange.close()
|
||||
}
|
||||
|
||||
val checker = UpdateChecker(
|
||||
apiUrl = "http://127.0.0.1:$serverPort/releases/latest",
|
||||
)
|
||||
|
||||
val result = checker.checkForUpdate(currentVersion = "2.0.0")
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHttpErrorGracefullyReturnsNull() = runBlocking {
|
||||
server.createContext("/releases/latest") { exchange ->
|
||||
exchange.sendResponseHeaders(404, 0)
|
||||
exchange.close()
|
||||
}
|
||||
|
||||
val checker = UpdateChecker(
|
||||
apiUrl = "http://127.0.0.1:$serverPort/releases/latest",
|
||||
)
|
||||
|
||||
val result = checker.checkForUpdate(currentVersion = "1.0.0")
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMalformedJsonGracefullyReturnsNull() = runBlocking {
|
||||
server.createContext("/releases/latest") { exchange ->
|
||||
val bytes = "Not valid JSON".toByteArray()
|
||||
exchange.sendResponseHeaders(200, bytes.size.toLong())
|
||||
exchange.responseBody.write(bytes)
|
||||
exchange.close()
|
||||
}
|
||||
|
||||
val checker = UpdateChecker(
|
||||
apiUrl = "http://127.0.0.1:$serverPort/releases/latest",
|
||||
)
|
||||
|
||||
val result = checker.checkForUpdate(currentVersion = "1.0.0")
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFallbackUrlUsedWhenHtmlUrlMissing() = runBlocking {
|
||||
val jsonResponse = """
|
||||
{
|
||||
"id": 5,
|
||||
"tag_name": "v1.0.2",
|
||||
"name": "WoW Backup v1.0.2"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
server.createContext("/releases/latest") { exchange ->
|
||||
val bytes = jsonResponse.toByteArray()
|
||||
exchange.sendResponseHeaders(200, bytes.size.toLong())
|
||||
exchange.responseBody.write(bytes)
|
||||
exchange.close()
|
||||
}
|
||||
|
||||
val checker = UpdateChecker(
|
||||
apiUrl = "http://127.0.0.1:$serverPort/releases/latest",
|
||||
fallbackUrl = "https://git.asarius.site/rukira/wow-backup/releases",
|
||||
)
|
||||
|
||||
val result = checker.checkForUpdate(currentVersion = "1.0.0")
|
||||
assertNotNull(result)
|
||||
assertEquals("https://git.asarius.site/rukira/wow-backup/releases", result.releaseUrl)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue