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(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(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( """{"totalCount":0,"items":[]}""", ) assertEquals(0, response.totalCount) assertTrue(response.items!!.isEmpty()) } @Test fun `known followers has no cursor envelope`() { val response = json.decodeFromString( """{"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(future) assertEquals("c.test", response.items!!.single().handle) } }