package community.yuvomi.mobile import android.content.Context import android.webkit.WebView import androidx.core.content.FileProvider import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import java.io.File import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import org.json.JSONArray import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class BlobExportTest { @Test fun rejectsBlobExportFromOpaqueNativePageEvenWithClaimedOrigin() { val context = ApplicationProvider.getApplicationContext() context.getSharedPreferences("server", Context.MODE_PRIVATE).edit() .putString("origin", "https://family.example").commit() val file = File(File(context.cacheDir, "captures").apply { mkdirs() }, "blob-test.csv") val uri = FileProvider.getUriForFile(context, "${context.packageName}.files", file) val worker = Executors.newSingleThreadExecutor() try { ActivityScenario.launch(MainActivity::class.java).use { scenario -> lateinit var web: WebView val created = CountDownLatch(1) var blob = "" scenario.onActivity { // A real WebView with a controlled document, not a network mock. // The HTTPS upstream suite separately exercises the app's client. web = WebView(it) web.settings.javaScriptEnabled = true web.loadDataWithBaseURL( "https://family.example/", "Export test", "text/html", "UTF-8", null, ) } val deadline = System.nanoTime() + 10_000_000_000L var ready = false while (!ready && System.nanoTime() < deadline) { val checked = CountDownLatch(1) scenario.onActivity { web.evaluateJavascript("location.origin") { ready = it == "\"https://family.example\"" checked.countDown() } } assertTrue(checked.await(2, TimeUnit.SECONDS)) if (!ready) Thread.sleep(50) } assertTrue("Trusted document loaded", ready) scenario.onActivity { web.evaluateJavascript( "URL.createObjectURL(new Blob(['name,value\\nYuvomi,Grüße\\n'],{type:'text/csv'}))", ) { blob = JSONArray("[$it]").getString(0) created.countDown() } } assertTrue(created.await(10, TimeUnit.SECONDS)) assertTrue( "Blob inherits trusted origin: $blob", BlobTransfer.owns(ServerAddress.parse("https://family.example"), blob), ) assertNotNull( context.contentResolver.openOutputStream(uri, "w")?.also { it.close() }, ) val saved = CountDownLatch(1) var success = false scenario.onActivity { assertFalse( "Native page URL remains opaque despite loadDataWithBaseURL", ServerAddress.parse("https://family.example").owns(web.url.orEmpty()), ) BlobTransfer.save( ServerAddress.parse("https://family.example"), web, blob, context.contentResolver, uri, worker, { true }, ) { success = it saved.countDown() } } assertTrue(saved.await(50, TimeUnit.SECONDS)) assertFalse(success) assertEquals(0L, file.length()) scenario.onActivity { web.destroy() } } } finally { worker.shutdownNow() file.delete() } } }