aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/test
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-10 14:21:46 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-10 14:21:46 +0200
commit2f2a9a6542d2194e50ffba6f382e4fdffd481838 (patch)
tree5437330f9e8b963f8af0904f3c5d80d1afc6dc3b /packages/meshbay-android/app/src/test
parent914d2f0af848fb80d70d517901f44420a4e47764 (diff)
downloadmeshbay-2f2a9a6542d2194e50ffba6f382e4fdffd481838.tar.gz
feat(android): one backup destination, a group the account owns alone
Chosen once at the top of Android Sync for every kind; photos and contacts go into <folder>/<account>-photos and -contacts. Owner and sole member are checked at set-up and before every run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-android/app/src/test')
-rw-r--r--packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/DocSyncTest.kt52
-rw-r--r--packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/PhotoSyncTest.kt32
2 files changed, 54 insertions, 30 deletions
diff --git a/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/DocSyncTest.kt b/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/DocSyncTest.kt
index 276f1fd..f53110b 100644
--- a/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/DocSyncTest.kt
+++ b/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/DocSyncTest.kt
@@ -6,8 +6,11 @@ import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertThrows
import org.junit.Test
-import org.meshbay.client.phonesync.DocConfig
+import org.meshbay.client.phonesync.Destination
+import org.meshbay.client.phonesync.DestinationChannels
import org.meshbay.client.phonesync.DocPlan
+import org.json.JSONArray
+import org.meshbay.client.bridge.Refused
import java.util.TimeZone
class DocSyncTest {
@@ -16,11 +19,10 @@ class DocSyncTest {
private val oct9 = 1791547200000L
private fun settings(account: String = "bob", folder: String = "Backups") = JSONObject()
- .put("account", account).put("groupId", "g1").put("groupName", "Mine").put("owner", "bob")
- .put("folder", folder)
+ .put("account", account).put("groupId", "g1").put("groupName", "Mine").put("folder", folder)
@Test fun `each kind goes into a folder of its own named for the account`() {
- val c = DocConfig.fromJson(settings(folder = "/Backups/Phone/"))
+ val c = Destination.fromJson(settings(folder = "/Backups/Phone/"))
assertEquals("Backups/Phone/bob-contacts", DocPlan.dirFor(c, "contacts"))
assertEquals("Backups/Phone/bob-messages", DocPlan.dirFor(c, "messages"))
}
@@ -33,23 +35,47 @@ class DocSyncTest {
@Test fun `a folder that climbs out or an account that is not a name is refused`() {
for (bad in listOf("../etc", "Backups/../x", "Backups//x", "./x", "")) {
- assertThrows(bad, IllegalArgumentException::class.java) { DocConfig.fromJson(settings(folder = bad)) }
+ assertThrows(bad, IllegalArgumentException::class.java) { Destination.fromJson(settings(folder = bad)) }
}
for (bad in listOf("../bob", "bob/x", "", "-bob")) {
- assertThrows(bad, IllegalArgumentException::class.java) { DocConfig.fromJson(settings(account = bad)) }
+ assertThrows(bad, IllegalArgumentException::class.java) { Destination.fromJson(settings(account = bad)) }
}
}
@Test fun `settings survive being stored`() {
- val c = DocConfig.fromJson(settings())
- assertEquals(c, DocConfig.parse(c.toJson().toString()))
- assertNull(DocConfig.parse("{not json"))
+ val c = Destination.fromJson(settings())
+ assertEquals(c, Destination.parse(c.toJson().toString()))
+ assertNull(Destination.parse("{not json"))
}
@Test fun `another group or folder is another backup`() {
- val a = DocConfig.fromJson(settings())
- assertNotEquals(a.ledgerKey, DocConfig.fromJson(settings(folder = "Other")).ledgerKey)
- assertNotEquals(a.ledgerKey, DocConfig.fromJson(settings().put("groupId", "g2")).ledgerKey)
- assertEquals(a.ledgerKey, DocConfig.fromJson(settings().put("groupName", "Renamed")).ledgerKey)
+ val a = Destination.fromJson(settings())
+ assertNotEquals(a.ledgerKey, Destination.fromJson(settings(folder = "Other")).ledgerKey)
+ assertNotEquals(a.ledgerKey, Destination.fromJson(settings().put("groupId", "g2")).ledgerKey)
+ assertEquals(a.ledgerKey, Destination.fromJson(settings().put("groupName", "Renamed")).ledgerKey)
+ }
+
+ @Test fun `every kind is told when the destination moves, and only then`() {
+ val store = DestinationChannels(FakePrefs())
+ var told = 0
+ store.onChange { told++ }
+ store.call("phonesync:set-destination", JSONArray().put(settings()))
+ assertEquals(1, told)
+ store.call("phonesync:set-destination", JSONArray().put(settings().put("groupName", "Renamed")))
+ assertEquals("a new name for the same place is not a new backup", 1, told)
+ store.call("phonesync:set-destination", JSONArray().put(settings(folder = "Other")))
+ assertEquals(2, told)
+ store.call("phonesync:set-destination", JSONArray())
+ assertEquals(3, told)
+ assertNull(store.get())
+ }
+
+ @Test fun `a destination the page got wrong is refused and the old one kept`() {
+ val store = DestinationChannels(FakePrefs())
+ store.call("phonesync:set-destination", JSONArray().put(settings()))
+ assertThrows(Refused::class.java) {
+ store.call("phonesync:set-destination", JSONArray().put(settings(folder = "../x")))
+ }
+ assertEquals("Backups", store.get()!!.folder)
}
}
diff --git a/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/PhotoSyncTest.kt b/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/PhotoSyncTest.kt
index 0e2e924..7c0b794 100644
--- a/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/PhotoSyncTest.kt
+++ b/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/PhotoSyncTest.kt
@@ -5,11 +5,11 @@ import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
-import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
+import org.meshbay.client.phonesync.Destination
import org.meshbay.client.photos.Photo
import org.meshbay.client.photos.PhotoLedger
import org.meshbay.client.photos.PhotoPlan
@@ -29,22 +29,24 @@ class PhotoSyncTest {
Photo(id, name, size, taken, added, modified, bucket, mime)
private fun config(includeExisting: Boolean = true, since: Long = 0, albums: List<String> = listOf("cam")) =
- SyncConfig("bob", "g1", "Family", "alice", "Media/Photos/Bob", albums, includeExisting, since)
+ SyncConfig(albums, includeExisting, since)
+
+ private val base = PhotoPlan.baseFor(Destination("bob", "g1", "Mine", "Backups"))
private fun entry(p: Photo, sha: String = "h", name: String = p.displayName) =
PhotoLedger.Entry(p.mediaId, p.modified, p.size, sha, "d", name, 1)
private fun plan(photos: List<Photo>, c: SyncConfig = config(), ledger: Map<Long, PhotoLedger.Entry> = emptyMap(),
same: Boolean = false) =
- PhotoPlan.plan(photos, c, { ledger[it] }, utc) { _, _ -> same }
+ PhotoPlan.plan(photos, c, base, { ledger[it] }, utc) { _, _ -> same }
// ── what is sent ──────────────────────────────────────────────────────────
@Test fun `everything already on the phone is sent, newest first, under its year and month`() {
val out = plan(listOf(photo(1, taken = oct9 - 40L * 86400000), photo(2), photo(3, taken = oct9 - 86400000)))
assertEquals(listOf(2L, 3L, 1L), out.map { it.photo.mediaId })
- assertEquals("Media/Photos/Bob/2026/2026-10", out[0].dir)
- assertEquals("Media/Photos/Bob/2026/2026-08", out[2].dir)
+ assertEquals("Backups/bob-photos/2026/2026-10", out[0].dir)
+ assertEquals("Backups/bob-photos/2026/2026-08", out[2].dir)
assertEquals("IMG_2.jpg", out[0].name)
}
@@ -79,7 +81,7 @@ class PhotoSyncTest {
assertEquals(1, out.size)
assertTrue(out[0].edited)
assertEquals("IMG_1-edited-20261009-130000.jpg", out[0].name)
- assertEquals("Media/Photos/Bob/2026/2026-10", out[0].dir)
+ assertEquals("Backups/bob-photos/2026/2026-10", out[0].dir)
}
@Test fun `a touch that left the bytes alone sends nothing`() {
@@ -92,8 +94,8 @@ class PhotoSyncTest {
@Test fun `the bytes are only read when the date moved and the size did not`() {
val before = photo(1)
var asked = 0
- PhotoPlan.plan(listOf(before), config(), { entry(before) }, utc) { _, _ -> asked++; true }
- PhotoPlan.plan(listOf(before.copy(size = 7, modified = 9)), config(), { entry(before) }, utc) { _, _ -> asked++; true }
+ PhotoPlan.plan(listOf(before), config(), base, { entry(before) }, utc) { _, _ -> asked++; true }
+ PhotoPlan.plan(listOf(before.copy(size = 7, modified = 9)), config(), base, { entry(before) }, utc) { _, _ -> asked++; true }
assertEquals(0, asked)
}
@@ -126,19 +128,15 @@ class PhotoSyncTest {
// ── settings from the page ───────────────────────────────────────────────
- @Test fun `a folder that is not one of the group's own is refused`() {
- for (bad in listOf("", "../etc", "Media/../x", "Media//x", "/")) {
- val o = JSONObject().put("account", "bob").put("groupId", "g1").put("folder", bad)
- assertThrows(bad, IllegalArgumentException::class.java) { SyncConfig.fromJson(o, 0) }
- }
+ @Test fun `photos go into the account's own folder under the destination`() {
+ assertEquals("Backups/bob-photos", base)
}
- @Test fun `changing the albums keeps the starting point, changing the group moves it`() {
- val o = JSONObject().put("account", "bob").put("groupId", "g1").put("folder", "Media/Photos")
- .put("albums", JSONArray(listOf("cam"))).put("includeExisting", false)
+ @Test fun `changing the albums keeps the starting point, changing the scope moves it`() {
+ val o = JSONObject().put("albums", JSONArray(listOf("cam"))).put("includeExisting", false)
val first = SyncConfig.fromJson(o, 100)
assertEquals(100, SyncConfig.fromJson(o.put("albums", JSONArray(listOf("cam", "x"))), 200, first).since)
- assertEquals(300, SyncConfig.fromJson(o.put("groupId", "g2"), 300, first).since)
+ assertEquals(300, SyncConfig.fromJson(o.put("includeExisting", true), 300, first).since)
}
@Test fun `settings survive being stored`() {