From 7e04941bccb2683f8a6e3ee38a99c50129234dd1 Mon Sep 17 00:00:00 2001 From: Lena Date: Thu, 1 Jan 2026 00:00:00 +0000 Subject: rsend: push phone folders to a home SSH host over rsync A small Android app for one-way folder backup, a KISS alternative to Syncthing. It bundles rsync (built from pinned source via the NDK) and a pure-Go SSH transport, both shipped in the APK as lib*.so and run from the native library directory. rsend pins the host key, stores the ed25519 identity Keystore-encrypted, pushes each folder additively or as a mirror, and runs on demand or on a WiFi-only schedule. The build is self-contained and reproducible: make setup provisions the toolchain, make builds rsync, rsh, and the APK. --- app/src/test/java/invalid/lena/rsend/ConfigTest.kt | 26 ++++++++++++ .../java/invalid/lena/rsend/RsyncRunnerTest.kt | 49 ++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 app/src/test/java/invalid/lena/rsend/ConfigTest.kt create mode 100644 app/src/test/java/invalid/lena/rsend/RsyncRunnerTest.kt (limited to 'app/src/test/java') diff --git a/app/src/test/java/invalid/lena/rsend/ConfigTest.kt b/app/src/test/java/invalid/lena/rsend/ConfigTest.kt new file mode 100644 index 0000000..51796ba --- /dev/null +++ b/app/src/test/java/invalid/lena/rsend/ConfigTest.kt @@ -0,0 +1,26 @@ +package invalid.lena.rsend + +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Test + +class ConfigTest { + + @Test + fun roundTrip() { + val c = Config( + remote = Remote("home", 2222, "backup"), + schedule = Schedule(enabled = true, intervalMinutes = 30, wifiOnly = false, requireCharging = true), + folders = listOf( + Folder("DCIM", "/storage/emulated/0/DCIM", "/b/DCIM", false, listOf(".thumbnails/")), + Folder("W", "/w", "/b/w", true, emptyList()), + ), + ) + assertEquals(c, Config.fromJson(c.toJson())) + } + + @Test + fun defaultsForEmptyObject() { + assertEquals(Config(), Config.fromJson(JSONObject("{}"))) + } +} diff --git a/app/src/test/java/invalid/lena/rsend/RsyncRunnerTest.kt b/app/src/test/java/invalid/lena/rsend/RsyncRunnerTest.kt new file mode 100644 index 0000000..ef7958f --- /dev/null +++ b/app/src/test/java/invalid/lena/rsend/RsyncRunnerTest.kt @@ -0,0 +1,49 @@ +package invalid.lena.rsend + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class RsyncRunnerTest { + + private val remote = Remote("host", 22, "user") + + @Test + fun basicArgs() { + val a = RsyncRunner.args("RSH", remote, Folder(name = "n", local = "/a", remote = "/b")) + assertEquals( + listOf( + "-rt", "--partial", "--timeout=300", "--mkpath", + "--no-perms", "--no-owner", "--no-group", "--omit-dir-times", + "-e", "RSH", "/a/", "user@host:/b/", + ), + a, + ) + } + + @Test + fun mirrorAddsDelete() { + val a = RsyncRunner.args("RSH", remote, Folder(local = "/a", remote = "/b", delete = true)) + assertTrue(a.contains("--delete")) + } + + @Test + fun additiveOmitsDelete() { + val a = RsyncRunner.args("RSH", remote, Folder(local = "/a", remote = "/b", delete = false)) + assertTrue(!a.contains("--delete")) + } + + @Test + fun excludesBecomeFlags() { + val a = RsyncRunner.args("RSH", remote, Folder(local = "/a", remote = "/b", excludes = listOf(".x/", ".y"))) + assertTrue(a.contains("--exclude=.x/")) + assertTrue(a.contains("--exclude=.y")) + } + + @Test + fun trailingSlashIdempotent() { + val a = RsyncRunner.args("RSH", remote, Folder(local = "/a/", remote = "/b/")) + assertTrue(a.contains("/a/")) + assertTrue(a.contains("user@host:/b/")) + } +} -- cgit v1.2.3