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
package social.grain
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import social.grain.data.api.GetFollowersResponse
import social.grain.data.api.GetGalleryFavoritesResponse
import social.grain.data.api.GetKnownFollowersResponse
/**
* The four actor lists share one model, so a field typed wrong breaks all of
* them at once — and only at runtime, as an empty list with a decode error in
* the log. The follower fixture is a real response captured from the appview.
*/
class ActorListDecodeTest {
private val json = Json {
ignoreUnknownKeys = true
explicitNulls = false
coerceInputValues = true
}
private val followersFixture = """
{
"totalCount": 5,
"items": [
{
"did": "did:plc:bcgltzqazw5tb6k2g3ttenbj",
"handle": "chadtmiller.com",
"displayName": "chad",
"description": "building @grain.social, suuuup",
"avatar": "https://cdn.grain.social/x/avatar/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/y"
},
{
"did": "did:plc:z6sonfx6nicq2ndrixghcolb",
"handle": "jj.mcdonough.rodeo",
"displayName": "JJ",
"description": "Data guy. Tryhard. VT, US. Born at 341ppm.",
"avatar": "https://cdn.grain.social/z/avatar/plain/did:plc:z6sonfx6nicq2ndrixghcolb/w"
}
],
"cursor": "Mjo6YmFmeXJlaWJjNWVhdHIzcnJrNXVjdGhmbDZkZHB6ZHF2cng3bHRsZWRnc3p3N2gzeXlwam9scGpjeWE"
}
""".trimIndent()
@Test
fun `a real followers response decodes`() {
val response = json.decodeFromString<GetFollowersResponse>(followersFixture)
assertEquals(5, response.totalCount)
assertEquals(2, response.items?.size)
assertNotNull("cursor drives pagination", response.cursor)
val first = response.items!!.first()
assertEquals("did:plc:bcgltzqazw5tb6k2g3ttenbj", first.did)
assertEquals("chadtmiller.com", first.handle)
assertEquals("chad", first.displayName)
assertTrue(first.avatar!!.startsWith("https://"))
}
@Test
fun `a member with no display name or bio still decodes`() {
// The appview omits these rather than sending null, which is the case
// an over-strict model would choke on.
val sparse = """{"items":[{"did":"did:plc:sparse","handle":"a.test"}]}"""
val response = json.decodeFromString<GetFollowersResponse>(sparse)
val member = response.items!!.single()
assertEquals("did:plc:sparse", member.did)
assertNull(member.displayName)
assertNull(member.description)
assertNull(member.avatar)
assertNull("no cursor means no further pages", response.cursor)
}
@Test
fun `an empty favorites list decodes rather than failing`() {
val response = json.decodeFromString<GetGalleryFavoritesResponse>(
"""{"totalCount":0,"items":[]}""",
)
assertEquals(0, response.totalCount)
assertTrue(response.items!!.isEmpty())
}
@Test
fun `known followers has no cursor envelope`() {
val response = json.decodeFromString<GetKnownFollowersResponse>(
"""{"items":[{"did":"did:plc:known","handle":"b.test","displayName":"Known"}]}""",
)
assertEquals("Known", response.items!!.single().displayName)
}
@Test
fun `a response with unknown fields still decodes`() {
// The appview adds fields ahead of clients; a strict decoder would fail
// the whole list on one new key.
val future = """{"items":[{"did":"did:plc:x","handle":"c.test","somethingNew":42}],"newTopLevel":true}"""
val response = json.decodeFromString<GetFollowersResponse>(future)
assertEquals("c.test", response.items!!.single().handle)
}
}