10007eb4fb
Build & Release APK / build (push) Successful in 12m50s
- Interruption: failed mid-write leaves original intact (no truncation, no temp leftover); a sync that drops after N files resumes cleanly on the next sync with all content byte-intact (real network-drop simulation). - SFTP: live round-trip test against an SFTP server (connect/upload-atomic/ list/download/overwrite/special-name/delete); skips if endpoint unreachable. - Scheduling: WorkManager request builders map Wi-Fi-only -> UNMETERED, charging-only -> requiresCharging, interval, input data, and tags correctly.
70 lines
3.2 KiB
Kotlin
70 lines
3.2 KiB
Kotlin
package com.syncflow
|
|
|
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
import androidx.test.platform.app.InstrumentationRegistry
|
|
import com.syncflow.data.providers.sftp.SftpProvider
|
|
import com.syncflow.data.security.CredentialStore
|
|
import com.syncflow.domain.model.CloudAccount
|
|
import com.syncflow.domain.model.ProviderType
|
|
import kotlinx.coroutines.runBlocking
|
|
import org.junit.Assert.assertEquals
|
|
import org.junit.Assert.assertTrue
|
|
import org.junit.Assume.assumeTrue
|
|
import org.junit.Test
|
|
import org.junit.runner.RunWith
|
|
import java.io.ByteArrayInputStream
|
|
import java.io.ByteArrayOutputStream
|
|
|
|
/**
|
|
* Live SFTP test (the other major provider code path: sshj). Runs against a throwaway SFTP
|
|
* server. Skips unless -e sftpHost/sftpPort/sftpUser/sftpPass are provided.
|
|
*/
|
|
@RunWith(AndroidJUnit4::class)
|
|
class SftpProviderTest {
|
|
|
|
private val ctx = InstrumentationRegistry.getInstrumentation().targetContext
|
|
private val args = InstrumentationRegistry.getArguments()
|
|
|
|
private fun provider() = SftpProvider(
|
|
CloudAccount(
|
|
id = 1, displayName = "sftp", email = null, providerType = ProviderType.SFTP,
|
|
credentialJson = """{"username":"${args.getString("sftpUser")}","password":"${args.getString("sftpPass")}"}""",
|
|
serverUrl = args.getString("sftpHost"), port = args.getString("sftpPort")?.toInt(),
|
|
),
|
|
CredentialStore(ctx),
|
|
)
|
|
|
|
@Test fun sftpFullRoundTrip() = runBlocking {
|
|
assumeTrue("sftp* args required", args.getString("sftpHost") != null)
|
|
val p = provider()
|
|
val dir = "upload/it_${System.currentTimeMillis()}"
|
|
|
|
// Skip (don't fail) if the endpoint isn't reachable from the test runner's network —
|
|
// e.g. a phone on an isolated VLAN that only reaches services via the reverse proxy.
|
|
assumeTrue("SFTP endpoint not reachable from this device's network", p.testConnection().isSuccess)
|
|
assertTrue("mkdir", p.createDirectory(dir).isSuccess)
|
|
|
|
// upload (atomic temp + rename), list, download
|
|
val body = "sftp round-trip ✓".toByteArray()
|
|
assertTrue("upload", p.uploadFile(ByteArrayInputStream(body), "$dir/f.txt", body.size.toLong()).isSuccess)
|
|
assertTrue("f.txt" in p.listFiles(dir).getOrThrow().map { it.name })
|
|
val out = ByteArrayOutputStream(); p.downloadFile("$dir/f.txt", out).getOrThrow()
|
|
assertEquals("sftp round-trip ✓", out.toString("UTF-8"))
|
|
|
|
// atomic overwrite (temp + rename over existing)
|
|
val v2 = "updated-content".toByteArray()
|
|
assertTrue(p.uploadFile(ByteArrayInputStream(v2), "$dir/f.txt", v2.size.toLong()).isSuccess)
|
|
val out2 = ByteArrayOutputStream(); p.downloadFile("$dir/f.txt", out2).getOrThrow()
|
|
assertEquals("updated-content", out2.toString("UTF-8"))
|
|
|
|
// special / non-ASCII name (SFTP handles UTF-8 natively, no URL encoding)
|
|
val special = "café & rapport (1).txt"
|
|
assertTrue(p.uploadFile(ByteArrayInputStream("x".toByteArray()), "$dir/$special", 1).isSuccess)
|
|
assertTrue(special in p.listFiles(dir).getOrThrow().map { it.name })
|
|
|
|
// delete
|
|
assertTrue(p.deleteFile("$dir/f.txt").isSuccess)
|
|
assertTrue("f.txt" !in p.listFiles(dir).getOrThrow().map { it.name })
|
|
}
|
|
}
|