package org.meshbay.client import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNull import org.junit.Assert.assertThrows import org.junit.Test 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 { private val utc = TimeZone.getTimeZone("UTC") // 2026-10-09 12:00:00 UTC private val oct9 = 1791547200000L private fun settings(account: String = "bob", folder: String = "Backups") = JSONObject() .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 = Destination.fromJson(settings(folder = "/Backups/Phone/")) assertEquals("Backups/Phone/bob-contacts", DocPlan.dirFor(c, "contacts")) assertEquals("Backups/Phone/bob-messages", DocPlan.dirFor(c, "messages")) } @Test fun `a file is named for when it was taken, in the phone's time`() { assertEquals("contacts-2026-10-09-1200.vcf", DocPlan.nameFor("contacts", "vcf", oct9, utc)) assertEquals("contacts-2026-10-09-1400.vcf", DocPlan.nameFor("contacts", "vcf", oct9, TimeZone.getTimeZone("Europe/Paris"))) } @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) { Destination.fromJson(settings(folder = bad)) } } for (bad in listOf("../bob", "bob/x", "", "-bob")) { assertThrows(bad, IllegalArgumentException::class.java) { Destination.fromJson(settings(account = bad)) } } } @Test fun `settings survive being stored`() { 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 = 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) } }