Skip to content

Commit bf05e1c

Browse files
committed
fix(server):make fs write operation async (to prevent UI freeze); add test for large files
1 parent c1603a5 commit bf05e1c

File tree

6 files changed

+220
-159
lines changed

6 files changed

+220
-159
lines changed

app/src/androidTest/java/com/matanh/transfer/AppFlowTest.kt

Lines changed: 129 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.matanh.transfer
22

33
import android.content.Context
4-
import android.net.Uri
54
import android.util.Log
65
import android.view.View
76
import android.widget.AutoCompleteTextView
@@ -22,6 +21,14 @@ import androidx.test.uiautomator.*
2221
import com.matanh.transfer.ui.SetupActivity
2322
import com.matanh.transfer.util.Constants
2423
import com.matanh.transfer.util.FileAdapter
24+
import kotlinx.serialization.json.Json
25+
import kotlinx.serialization.json.JsonArray
26+
import kotlinx.serialization.json.JsonObject
27+
import kotlinx.serialization.json.contentOrNull
28+
import kotlinx.serialization.json.jsonArray
29+
import kotlinx.serialization.json.jsonObject
30+
import kotlinx.serialization.json.jsonPrimitive
31+
import kotlinx.serialization.json.longOrNull
2532
import okhttp3.*
2633
import okhttp3.MediaType.Companion.toMediaType
2734
import okhttp3.RequestBody.Companion.toRequestBody
@@ -30,20 +37,14 @@ import org.junit.*
3037
import org.junit.Assume.assumeTrue
3138
import org.junit.runner.RunWith
3239
import org.junit.runners.MethodSorters
40+
import java.io.File
3341
import java.io.IOException
42+
import java.io.RandomAccessFile
3443
import java.net.URLDecoder
35-
import java.util.concurrent.TimeUnit
36-
import java.util.regex.Pattern
3744
import java.net.URLEncoder
3845
import java.nio.charset.StandardCharsets
39-
import kotlinx.serialization.json.Json
40-
import kotlinx.serialization.json.JsonArray
41-
import kotlinx.serialization.json.JsonObject
42-
import kotlinx.serialization.json.contentOrNull
43-
import kotlinx.serialization.json.jsonArray
44-
import kotlinx.serialization.json.jsonObject
45-
import kotlinx.serialization.json.jsonPrimitive
46-
import kotlinx.serialization.json.longOrNull
46+
import java.util.concurrent.TimeUnit
47+
import java.util.regex.Pattern
4748

4849
// --- Helpers to keep JSON access clean ---
4950
fun JsonObject.string(key: String): String? =
@@ -60,7 +61,8 @@ fun JsonObject.array(key: String): JsonArray? =
6061

6162

6263
fun String.encodeURL(): String =
63-
URLEncoder.encode(this, StandardCharsets.UTF_8.name()).replace("+","%20")
64+
URLEncoder.encode(this, StandardCharsets.UTF_8.name()).replace("+", "%20")
65+
6466
fun String.decodeURL(): String =
6567
URLDecoder.decode(this, StandardCharsets.UTF_8.name())
6668

@@ -161,7 +163,11 @@ class AppFlowTest {
161163
synchronized(createdFiles) { createdFiles.add(filename) }
162164
}
163165

164-
private fun uploadFileHttp(encodedFilename: String, content: String,mimetype:String="text/plain"): Boolean {
166+
private fun uploadFileHttp(
167+
encodedFilename: String,
168+
content: String,
169+
mimetype: String = "text/plain"
170+
): Boolean {
165171
val requestBody = content.toRequestBody(mimetype.toMediaType())
166172
val request = Request.Builder().url("$serverUrl/$encodedFilename").put(requestBody).build()
167173
client.newCall(request).execute().use { resp ->
@@ -172,14 +178,20 @@ class AppFlowTest {
172178
return false
173179
}
174180
}
175-
private fun checkFileContent(filenameEncoded: String, expectedContent: String? = null): Boolean {
176-
val request = Request.Builder().url("$serverUrl/api/download/$filenameEncoded").get().build()
177-
client.newCall(request).execute().use { resp ->
178-
if (!resp.isSuccessful) return false
179-
if (expectedContent == null) return true
180-
val body = resp.body?.string() ?: return false
181-
return body == expectedContent
182-
}}
181+
182+
private fun checkFileContent(
183+
filenameEncoded: String,
184+
expectedContent: String? = null
185+
): Boolean {
186+
val request =
187+
Request.Builder().url("$serverUrl/api/download/$filenameEncoded").get().build()
188+
client.newCall(request).execute().use { resp ->
189+
if (!resp.isSuccessful) return false
190+
if (expectedContent == null) return true
191+
val body = resp.body?.string() ?: return false
192+
return body == expectedContent
193+
}
194+
}
183195

184196
private fun getFilesJson(): JsonObject? {
185197
val request = Request.Builder().url("$serverUrl/api/files").get().build()
@@ -188,7 +200,8 @@ class AppFlowTest {
188200
if (!resp.isSuccessful) return null;
189201
val body = resp.body?.string() ?: return null
190202
return json.parseToJsonElement(body).jsonObject
191-
}}
203+
}
204+
}
192205

193206

194207
@Test
@@ -307,7 +320,7 @@ class AppFlowTest {
307320

308321

309322
// --- Step 3: Upload a file via HTTP ---
310-
uploadFileHttp(testFileName, testFileContent,"text/plain")
323+
uploadFileHttp(testFileName, testFileContent, "text/plain")
311324

312325

313326
// --- Step 4: Verify the file appears in the RecyclerView ---
@@ -331,7 +344,10 @@ class AppFlowTest {
331344
}
332345
}
333346
Assert.assertTrue("Uploaded file did not appear in the UI.", isFileVisible)
334-
Assert.assertTrue("file content is not as expected",checkFileContent(testFileName, testFileContent))
347+
Assert.assertTrue(
348+
"file content is not as expected",
349+
checkFileContent(testFileName, testFileContent)
350+
)
335351
}
336352

337353
@Test
@@ -371,7 +387,7 @@ class AppFlowTest {
371387
}
372388

373389
@Test
374-
fun testD_FilenameEncoding(){
390+
fun testD_FilenameEncoding() {
375391
assumeTrue("Server URL not set – did testA fail?", serverUrl != null)
376392

377393
val filename1 = "a+b c.py"
@@ -381,12 +397,12 @@ class AppFlowTest {
381397
uploadFileHttp(filename1.encodeURL(), content1)
382398
uploadFileHttp(filename2.encodeURL(), content2)
383399

384-
Assert.assertTrue(checkFileContent(filename1.encodeURL(),content1));
385-
Assert.assertTrue(checkFileContent(filename2.encodeURL(),content2));
400+
Assert.assertTrue(checkFileContent(filename1.encodeURL(), content1));
401+
Assert.assertTrue(checkFileContent(filename2.encodeURL(), content2));
386402

387403
val jsonObj = getFilesJson()
388404
val files = jsonObj?.array("files") ?: return
389-
val file1 = files.map { it.jsonObject }.firstOrNull { it.string("name") ==filename1 }
405+
val file1 = files.map { it.jsonObject }.firstOrNull { it.string("name") == filename1 }
390406
val file2 = files.map { it.jsonObject }.firstOrNull { it.string("name") == filename2 }
391407
Assert.assertNotNull("File 'a+b c.py' not found in JSON", file1)
392408
Assert.assertNotNull("File 'a b+c.py' not found in JSON", file2)
@@ -400,4 +416,89 @@ class AppFlowTest {
400416
)
401417
}
402418

419+
@Test
420+
fun testE_largeFiles() {
421+
assumeTrue("Server URL not set – did testA fail?", serverUrl != null)
422+
423+
val largeFileName = "large_test_1gb.bin"
424+
val largeFileSize: Long = 1024L * 1024L * 1024L // 1 GB
425+
// IMPORTANT: This may take several seconds and requires sufficient storage space on the device/emulator.
426+
val tempFile = createLargeFile(largeFileName, largeFileSize)
427+
// addToCreated(largeFileName) // Ensure it's cleaned up in tearDownClass
428+
// --- Step 1: Upload the large file via HTTP ---
429+
val requestBody = RequestBody.create(
430+
"application/octet-stream".toMediaType(),
431+
tempFile
432+
)
433+
434+
val encodedFilename = largeFileName.encodeURL()
435+
val request = Request.Builder()
436+
.url("$serverUrl/$encodedFilename")
437+
.put(requestBody)
438+
.build()
439+
440+
441+
Log.i(
442+
"LargeUploadTest", "Uploading $largeFileName (${largeFileSize} bytes)"
443+
)
444+
val startTime = System.currentTimeMillis()
445+
446+
447+
client.newCall(request).execute().use { resp ->
448+
val duration = System.currentTimeMillis() - startTime
449+
450+
Assert.assertTrue(
451+
"Large file upload failed. HTTP code: ${resp.code}",
452+
resp.isSuccessful
453+
)
454+
addToCreated(largeFileName)
455+
Log.i("LargeUploadTest", "Upload of $largeFileName took $duration s.")
456+
457+
}
458+
// --- Step 2: Verify the file appears in the RecyclerView ---
459+
await.atMost(10, TimeUnit.SECONDS).ignoreExceptions().untilAsserted {
460+
onView(withId(R.id.rvFiles))
461+
.perform(
462+
RecyclerViewActions.scrollTo<FileAdapter.ViewHolder>(
463+
hasDescendant(withText(largeFileName))
464+
)
465+
)
466+
onView(withText(largeFileName)).check(matches(isDisplayed()))
467+
}
468+
val filesJson = getFilesJson()
469+
val files = filesJson?.array("files")
470+
val largeFileEntry =
471+
files?.map { it.jsonObject }?.firstOrNull { it.string("name") == largeFileName }
472+
Assert.assertNotNull("Large file not found in /api/files JSON response.", largeFileEntry)
473+
Assert.assertEquals(
474+
"Reported file size mismatch.",
475+
largeFileSize,
476+
largeFileEntry?.long("size")
477+
)
478+
tempFile.delete()
479+
480+
}
481+
482+
private fun createLargeFile(filename: String, sizeBytes: Long): File {
483+
val context = ApplicationProvider.getApplicationContext<Context>()
484+
// Use the app's cache directory for a temporary file
485+
val tempFile = File(context.cacheDir, filename)
486+
487+
if (tempFile.exists()) {
488+
tempFile.delete()
489+
}
490+
491+
// Use RandomAccessFile and FileChannel to quickly size the file without holding 1GB in memory
492+
RandomAccessFile(tempFile, "rw").use { raf ->
493+
// This sets the file size without writing all the data
494+
raf.setLength(sizeBytes)
495+
}
496+
val filelen = tempFile.length()
497+
Assert.assertTrue(
498+
"Failed to create file of correct size: $filelen (got $sizeBytes)",
499+
filelen == sizeBytes
500+
)
501+
return tempFile
502+
}
503+
403504
}

0 commit comments

Comments
 (0)