Files
GaCyGO-Android/app/src/test/java/com/gacy/android/GoRuntimeInstallerTest.kt
T

119 lines
5.7 KiB
Kotlin
Raw Normal View History

2026-11-02 02:39:37 +07:00
package com.gacy.android
import com.gacy.android.data.GoRuntimeInstaller
import org.json.JSONObject
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class GoRuntimeInstallerTest {
@get:Rule val tmp = TemporaryFolder()
private fun archive(resource: String, hotfix: String, extra: Pair<String, String>? = null, versions: String = "{}"): ByteArray {
val entries = linkedMapOf(
"persistent" to "original-save", "freesr-data.json" to "original-roster",
"server.json" to "server-config", "hotfix.json" to hotfix, "versions.json" to versions,
"res.json" to resource, "teleports.json" to "{}", "resources/MapEntrance.json" to "{}"
)
if (extra != null) entries[extra.first] = extra.second
val output = ByteArrayOutputStream()
ZipOutputStream(output).use { zip ->
entries.forEach { (name, content) ->
zip.putNextEntry(ZipEntry(name)); zip.write(content.toByteArray()); zip.closeEntry()
}
}
return output.toByteArray()
}
@Test fun updatePreservesUserDataAndRepairsMissingResources() {
val directory = tmp.newFolder("runtime")
val old = archive("old", """{"existing":{"asset":"bundled"}}""")
GoRuntimeInstaller(directory) { ByteArrayInputStream(old) }.prepare()
File(directory, "persistent").writeText("user-save")
File(directory, "freesr-data.json").writeText("user-roster")
File(directory, "hotfix.json").writeText("""{"existing":{"asset":"user"}}""")
File(directory, "versions.json").writeText("""{"existing":{"asset":"user"}}""")
val updated = archive("new", """{"existing":{"asset":"new"},"added":{"asset":"new"}}""", versions = """{"existing":{"asset":"new"},"added":{"asset":"new"}}""")
val installer = GoRuntimeInstaller(directory) { ByteArrayInputStream(updated) }
installer.prepare()
assertEquals("user-save", File(directory, "persistent").readText())
assertEquals("user-roster", File(directory, "freesr-data.json").readText())
assertEquals("new", File(directory, "res.json").readText())
val hotfix = JSONObject(File(directory, "hotfix.json").readText())
assertEquals("user", hotfix.getJSONObject("existing").getString("asset"))
assertEquals("new", hotfix.getJSONObject("added").getString("asset"))
val versions = JSONObject(File(directory, "versions.json").readText())
assertEquals("user", versions.getJSONObject("existing").getString("asset"))
assertEquals("new", versions.getJSONObject("added").getString("asset"))
File(directory, "resources/MapEntrance.json").delete()
installer.prepare()
assertTrue(File(directory, "resources/MapEntrance.json").isFile)
assertEquals("user-save", File(directory, "persistent").readText())
}
@Test fun rejectsPathsOutsideRuntimeDirectory() {
val directory = tmp.newFolder("runtime")
val zip = archive("data", "{}", "../outside" to "bad")
try {
GoRuntimeInstaller(directory) { ByteArrayInputStream(zip) }.prepare()
fail("Path traversal should be rejected")
} catch (_: IllegalArgumentException) { }
assertFalse(File(directory.parentFile, "outside").exists())
assertFalse(File(directory, ".asset-version").exists())
}
@Test fun installerUpgradeMergesVersionsEvenWhenArchiveIsUnchanged() {
val directory = tmp.newFolder("migration")
val zip = archive("data", "{}", versions = """{"new":{"asset":"bundle"}}""")
val installer = GoRuntimeInstaller(directory) { ByteArrayInputStream(zip) }
installer.prepare()
File(directory, "versions.json").writeText("""{"custom":{"asset":"user"}}""")
val marker = File(directory, ".asset-version")
marker.writeText(marker.readText().removePrefix("2:"))
installer.prepare()
val versions = JSONObject(File(directory, "versions.json").readText())
assertEquals("user", versions.getJSONObject("custom").getString("asset"))
assertEquals("bundle", versions.getJSONObject("new").getString("asset"))
}
@Test fun simultaneousStartsSerializeExtraction() {
val directory = tmp.newFolder("runtime")
val zip = archive("data", "{}")
val firstOpened = CountDownLatch(1)
val release = CountDownLatch(1)
val secondOpened = CountDownLatch(1)
val opens = AtomicInteger()
val installer = GoRuntimeInstaller(directory) {
if (opens.incrementAndGet() == 1) {
firstOpened.countDown()
check(release.await(5, TimeUnit.SECONDS))
} else {
secondOpened.countDown()
}
ByteArrayInputStream(zip)
}
val workers = Executors.newFixedThreadPool(2)
try {
val first = workers.submit { installer.prepare() }
assertTrue(firstOpened.await(5, TimeUnit.SECONDS))
val second = workers.submit { installer.prepare() }
assertFalse("a second install entered before the first was released", secondOpened.await(200, TimeUnit.MILLISECONDS))
release.countDown()
first.get(10, TimeUnit.SECONDS); second.get(10, TimeUnit.SECONDS)
assertEquals("data", File(directory, "res.json").readText())
} finally {
release.countDown()
workers.shutdownNow()
}
}
}