diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-10 14:21:46 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-10 14:21:46 +0200 |
| commit | 2f2a9a6542d2194e50ffba6f382e4fdffd481838 (patch) | |
| tree | 5437330f9e8b963f8af0904f3c5d80d1afc6dc3b /packages/meshbay-android/app/src/test | |
| parent | 914d2f0af848fb80d70d517901f44420a4e47764 (diff) | |
| download | meshbay-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.kt | 52 | ||||
| -rw-r--r-- | packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/PhotoSyncTest.kt | 32 |
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`() { |