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? = 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() } } }