SHA256
Add real native onboarding/auth/dashboard/tasks/notes/shopping, origin-bound HTTPS and encrypted AndroidKeyStore session persistence. Archive the superseded browser renderer and tests. Verify 10 JVM tests, 6 native API-35 emulator tests, lintDebug/lintRelease and debug/unsigned release APK/AAB builds; the real upstream suite passes 7 tests and exports a nonblank native fixture screenshot. Tink host-tool update removes the old generated-protobuf warning. Full module/visual parity remains explicitly incomplete.
112 lines
4.6 KiB
Kotlin
112 lines
4.6 KiB
Kotlin
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>()
|
|
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/",
|
|
"<!doctype html><title>Export test</title>",
|
|
"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()
|
|
}
|
|
}
|
|
}
|