aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/PushTest.kt
blob: c5759068ad02c7666111b0fa87c0c39ee3970887 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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<String, Any?>) =
        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") }
    }
}