11package com.matanh.transfer
22
33import android.content.Context
4- import android.net.Uri
54import android.util.Log
65import android.view.View
76import android.widget.AutoCompleteTextView
@@ -22,6 +21,14 @@ import androidx.test.uiautomator.*
2221import com.matanh.transfer.ui.SetupActivity
2322import com.matanh.transfer.util.Constants
2423import 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
2532import okhttp3.*
2633import okhttp3.MediaType.Companion.toMediaType
2734import okhttp3.RequestBody.Companion.toRequestBody
@@ -30,20 +37,14 @@ import org.junit.*
3037import org.junit.Assume.assumeTrue
3138import org.junit.runner.RunWith
3239import org.junit.runners.MethodSorters
40+ import java.io.File
3341import java.io.IOException
42+ import java.io.RandomAccessFile
3443import java.net.URLDecoder
35- import java.util.concurrent.TimeUnit
36- import java.util.regex.Pattern
3744import java.net.URLEncoder
3845import 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 ---
4950fun JsonObject.string (key : String ): String? =
@@ -60,7 +61,8 @@ fun JsonObject.array(key: String): JsonArray? =
6061
6162
6263fun String.encodeURL (): String =
63- URLEncoder .encode(this , StandardCharsets .UTF_8 .name()).replace(" +" ," %20" )
64+ URLEncoder .encode(this , StandardCharsets .UTF_8 .name()).replace(" +" , " %20" )
65+
6466fun 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