package org.meshbay.client 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.Test import org.meshbay.client.notify.Notifier import org.meshbay.client.notify.PushState class PushTest { private fun payload(vararg pairs: Pair) = JSONObject().apply { put("v", 1); pairs.forEach { (k, v) -> put(k, v ?: JSONObject.NULL) } } .toString().toByteArray() @Test fun `a conversation is one entry per group, anything else one per notification`() { val chat = Notifier.parse(payload("id" to 7, "kind" to "chat_message", "title" to "a posted in b", "group_id" to "g-1", "link" to "#/group/g-1"))!! assertEquals(Notifier.CHANNEL_CHAT, chat.channel) assertEquals("chat:g-1", chat.tag) assertEquals("#/group/g-1", chat.link) val invite = Notifier.parse(payload("id" to 8, "kind" to "group_invite", "title" to "x invited you", "group_id" to null, "link" to "#/"))!! assertEquals(Notifier.CHANNEL_OTHER, invite.channel) assertEquals("n:8", invite.tag) } @Test fun `a link that is not a route inside the page is dropped, not followed`() { for (bad in listOf("https://elsewhere.example/", "javascript:alert(1)", "#/x\";alert(1)//", "//host/#/")) { val shown = Notifier.parse(payload("id" to 1, "kind" to "k", "title" to "t", "link" to bad))!! assertNull(bad, shown.link) } } @Test fun `what is not ours to draw is not drawn`() { assertNull(Notifier.parse("not json".toByteArray())) assertNull(Notifier.parse(JSONObject().put("v", 2).put("title", "t").toString().toByteArray())) assertNull(Notifier.parse(payload("id" to 1, "kind" to "k", "title" to " "))) } private val sub = "0f8fad5b-d9cb-469f-a165-70867728950e" private val account = "3f2504e0-4f89-41d3-9a0c-0305e82c3301" private val secret = "Zm9vYmFyYmF6cXV4X3NlY3JldF92YWx1ZQ" private val since = "2026-10-09T10:00:00.123456+00:00" @Test fun `the hub is registered again when the distributor hands out a new endpoint`() { val state = PushState(FakePrefs()) state.endpoint("https://push.example.net/1", "k", "a") // not asked for: ignored assertEquals(PushState.OFF, state.status().getString("state")) state.requested() state.endpoint("https://push.example.net/1", "k", "a") state.remember(sub, account, secret, since) val s = state.status() assertEquals(s.getString("endpoint"), s.getString("registered")) state.endpoint("https://push.example.net/2", "k", "a") val moved = state.status() assertEquals("https://push.example.net/1", moved.getString("registered")) assertEquals("https://push.example.net/2", moved.getString("endpoint")) } @Test fun `no distributor, or one that gives no keys, means fetching and not failing`() { val state = PushState(FakePrefs()) state.requested() state.endpoint("https://push.example.net/1", null, null) val s = state.status() assertEquals(PushState.READY, s.getString("state")) assertEquals("NO_KEYS", s.getString("reason")) assertTrue(s.isNull("endpoint")) state.remember(sub, account, secret, since) assertEquals(PushState.PollTarget(sub, secret, since), state.pollTarget()) } @Test fun `the fetch cursor only moves forward`() { val state = PushState(FakePrefs()) state.requested() state.remember(sub, account, secret, since) state.advance("2026-10-09T09:00:00+00:00") assertEquals(since, state.pollTarget()!!.since) state.advance("2026-10-09T11:00:00+00:00") assertEquals("2026-10-09T11:00:00+00:00", state.pollTarget()!!.since) } @Test fun `a line pushed then fetched is drawn once, and a conversation moving on is news`() { val state = PushState(FakePrefs()) assertTrue(state.firstSight(7, "2026-10-09T10:00:00+00:00")) assertFalse(state.firstSight(7, "2026-10-09T10:00:00+00:00")) assertTrue(state.firstSight(7, "2026-10-09T10:05:00+00:00")) assertFalse(state.firstSight(7, "2026-10-09T10:01:00+00:00")) } @Test fun `a row the hub forgot stops the fetch until the page registers again`() { val state = PushState(FakePrefs()) state.requested() state.remember(sub, account, secret, since) state.forgetSubscription() assertNull(state.pollTarget()) } @Test fun `only ids are remembered`() { val state = PushState(FakePrefs()) assertThrows(IllegalArgumentException::class.java) { state.remember("../x", account, secret, since) } assertThrows(IllegalArgumentException::class.java) { state.remember(sub, "", secret, since) } assertThrows(IllegalArgumentException::class.java) { state.remember(sub, account, "short", since) } assertThrows(IllegalArgumentException::class.java) { state.remember(sub, account, secret, "yesterday") } } }