aboutsummaryrefslogtreecommitdiff
path: root/app/src/test/java/invalid
diff options
context:
space:
mode:
authorLena <lena@omega>2026-08-16 00:00:00 +0000
committerLena <lena@omega>2026-08-16 00:00:00 +0000
commit0890d139e5fa501561e469aca7854791a936fcb6 (patch)
tree58c6002cfb9f9b198a5013941ba01147aed1bc9a /app/src/test/java/invalid
parent6c0e672432dc466214c0838db378e8269d090ab4 (diff)
downloadrsend-0890d139e5fa501561e469aca7854791a936fcb6.tar.gz
app: support multiple named remotes
Let each folder select a named SSH endpoint and migrate the published 0.1.x configuration without discarding its folders or host pin.
Diffstat (limited to 'app/src/test/java/invalid')
-rw-r--r--app/src/test/java/invalid/lena/rsend/ConfigTest.kt111
-rw-r--r--app/src/test/java/invalid/lena/rsend/RsyncRunnerTest.kt12
2 files changed, 114 insertions, 9 deletions
diff --git a/app/src/test/java/invalid/lena/rsend/ConfigTest.kt b/app/src/test/java/invalid/lena/rsend/ConfigTest.kt
index a63582d..3449a8c 100644
--- a/app/src/test/java/invalid/lena/rsend/ConfigTest.kt
+++ b/app/src/test/java/invalid/lena/rsend/ConfigTest.kt
@@ -2,6 +2,8 @@ package invalid.lena.rsend
import org.json.JSONObject
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
import org.junit.Test
class ConfigTest {
@@ -9,11 +11,14 @@ class ConfigTest {
@Test
fun roundTrip() {
val c = Config(
- remote = Remote("home", 2222, "backup"),
+ remotes = listOf(
+ Remote("nas", "home", 2222, "backup", "home ssh-ed25519 AAAA"),
+ Remote("reader", "kobo.lan", 22, "sync"),
+ ),
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()),
+ Folder("DCIM", "/storage/emulated/0/DCIM", "nas", "/b/DCIM", false, listOf(".thumbnails/")),
+ Folder("Books", "/books", "reader", "/sd/books", true, emptyList()),
),
)
assertEquals(c, Config.fromJson(c.toJson()))
@@ -29,4 +34,104 @@ class ConfigTest {
val json = JSONObject("""{"schedule":{"intervalMinutes":1}}""")
assertEquals(Schedule.MIN_INTERVAL_MINUTES, Config.fromJson(json).schedule.intervalMinutes)
}
+
+ @Test
+ fun readsLegacySingleRemoteShape() {
+ val json = JSONObject(
+ """
+ {
+ "remote": {"host": "home", "port": 2222, "user": "backup"},
+ "folders": [
+ {"name": "DCIM", "local": "/d", "remote": "/b/DCIM", "delete": true, "excludes": [".x/"]}
+ ]
+ }
+ """
+ )
+ val c = Config.fromJson(json, legacyPin = "home ssh-ed25519 AAAA")
+ assertEquals(listOf(Remote("home", "home", 2222, "backup", "home ssh-ed25519 AAAA")), c.remotes)
+ assertEquals(
+ listOf(Folder("DCIM", "/d", "home", "/b/DCIM", true, listOf(".x/"))),
+ c.folders,
+ )
+ }
+
+ @Test
+ fun legacyShapeWithoutHostStaysEmpty() {
+ val json = JSONObject("""{"remote": {"host": "", "port": 22, "user": ""}}""")
+ assertEquals(emptyList<Remote>(), Config.fromJson(json, legacyPin = "unused").remotes)
+ }
+
+ // A 0.1.x config always wrote a "remote" object, even before the host was
+ // filled in. Such a config still has folders with real destination paths;
+ // migrating must keep them rather than drop them on the floor.
+ @Test
+ fun legacyShapeWithoutHostKeepsFolderPaths() {
+ val json = JSONObject(
+ """
+ {
+ "remote": {"host": "", "port": 22, "user": ""},
+ "folders": [
+ {"name": "DCIM", "local": "/d", "remote": "/b/DCIM", "delete": false, "excludes": []}
+ ]
+ }
+ """
+ )
+ val c = Config.fromJson(json)
+ assertEquals(listOf(Folder("DCIM", "/d", "", "/b/DCIM", false, emptyList())), c.folders)
+ }
+
+ // The migration must run exactly once. load() writes the converted config
+ // back, and the written shape must no longer look legacy, or a later read
+ // would migrate an already-migrated config and re-apply the legacy field
+ // mapping to fields that no longer carry those meanings.
+ @Test
+ fun migratingTwiceIsNotPossible() {
+ val legacy = JSONObject(
+ """
+ {
+ "remote": {"host": "home", "port": 2222, "user": "backup"},
+ "folders": [{"name": "DCIM", "local": "/d", "remote": "/b/DCIM", "delete": true, "excludes": []}]
+ }
+ """
+ )
+ assertTrue(Config.legacyShape(legacy))
+
+ val once = Config.fromJson(legacy, legacyPin = "home ssh-ed25519 AAAA")
+ val written = once.toJson()
+ assertFalse(Config.legacyShape(written))
+ assertEquals(once, Config.fromJson(written))
+ }
+
+ @Test
+ fun freshAndCurrentConfigsAreNotLegacy() {
+ assertFalse(Config.legacyShape(JSONObject("{}")))
+ assertFalse(Config.legacyShape(Config().toJson()))
+ assertFalse(Config.legacyShape(JSONObject("""{"remotes": [], "remote": {"host": "x"}}""")))
+ }
+
+ // An empty remotes array is still the new shape: a 0.2.x user who deleted
+ // their only remote must not be dragged back through the migration.
+ @Test
+ fun emptyRemotesArrayIsNotLegacy() {
+ val json = JSONObject(
+ """{"remotes": [], "folders": [{"name": "DCIM", "local": "/d", "remotePath": "/b", "remoteName": "gone"}]}"""
+ )
+ assertFalse(Config.legacyShape(json))
+ val c = Config.fromJson(json, legacyPin = "should be ignored")
+ assertEquals(emptyList<Remote>(), c.remotes)
+ assertEquals(listOf(Folder("DCIM", "/d", "gone", "/b", false, emptyList())), c.folders)
+ }
+
+ @Test
+ fun syncReadyNeedsKeyFolderAndPinnedRemote() {
+ val pinned = Remote("nas", "home", 22, "backup", "line")
+ val folder = Folder("d", "/d", "nas", "/b")
+ assertTrue(Config(remotes = listOf(pinned), folders = listOf(folder)).syncReady(keyExists = true))
+ assertFalse(Config(remotes = listOf(pinned), folders = listOf(folder)).syncReady(keyExists = false))
+ assertFalse(Config(remotes = listOf(pinned)).syncReady(keyExists = true))
+ assertFalse(
+ Config(remotes = listOf(pinned.copy(hostKey = "")), folders = listOf(folder))
+ .syncReady(keyExists = true)
+ )
+ }
}
diff --git a/app/src/test/java/invalid/lena/rsend/RsyncRunnerTest.kt b/app/src/test/java/invalid/lena/rsend/RsyncRunnerTest.kt
index 8edf9fa..9729f62 100644
--- a/app/src/test/java/invalid/lena/rsend/RsyncRunnerTest.kt
+++ b/app/src/test/java/invalid/lena/rsend/RsyncRunnerTest.kt
@@ -6,11 +6,11 @@ import org.junit.Test
class RsyncRunnerTest {
- private val remote = Remote("host", 22, "user")
+ private val remote = Remote("nas", "host", 22, "user")
@Test
fun basicArgs() {
- val a = RsyncRunner.args("RSH", remote, Folder(name = "n", local = "/a", remote = "/b"))
+ val a = RsyncRunner.args("RSH", remote, Folder(name = "n", local = "/a", remoteName = "nas", remotePath = "/b"))
assertEquals(
listOf(
"-rt", "--partial", "--timeout=300",
@@ -23,26 +23,26 @@ class RsyncRunnerTest {
@Test
fun mirrorAddsDelete() {
- val a = RsyncRunner.args("RSH", remote, Folder(local = "/a", remote = "/b", delete = true))
+ val a = RsyncRunner.args("RSH", remote, Folder(local = "/a", remotePath = "/b", delete = true))
assertTrue(a.contains("--delete"))
}
@Test
fun additiveOmitsDelete() {
- val a = RsyncRunner.args("RSH", remote, Folder(local = "/a", remote = "/b", delete = false))
+ val a = RsyncRunner.args("RSH", remote, Folder(local = "/a", remotePath = "/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")))
+ val a = RsyncRunner.args("RSH", remote, Folder(local = "/a", remotePath = "/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/"))
+ val a = RsyncRunner.args("RSH", remote, Folder(local = "/a/", remotePath = "/b/"))
assertTrue(a.contains("/a/"))
assertTrue(a.contains("user@host:/b/"))
}