package social.grain import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import social.grain.data.api.ProfileSeed import social.grain.data.api.ProfileSeed.Seeded import social.grain.support.StubServer import social.grain.support.testAuth import social.grain.support.xrpc /** * A grain profile made for the account at sign-in: left alone once it has a * name, made or put over from the Bluesky profile, made bare with neither, and * an unnamed one left with no Bluesky profile. */ @RunWith(AndroidJUnit4::class) class ProfileSeedTest { private val server = StubServer() private val client = server.xrpc() private val auth = testAuth() private val avatar = """{"${'$'}type":"blob","ref":{"${'$'}link":"bafk"},"mimeType":"image/png","size":9}""" private val notFound = """{"error":"RecordNotFound","message":"Could not locate record"}""" @Test fun aNamedGrainProfileIsLeftAlone() = runTest { profileReads(grain = """{"uri":"at://did:plc:me/social.grain.actor.profile/self","cid":"c1","value":{"displayName":"Me"}}""", bsky = BSKY) assertEquals(Seeded.Named, ProfileSeed.seed(client, auth)) assertEquals(1, server.countOf("com.atproto.repo.getRecord")) assertEquals(0, server.countOf("com.atproto.repo.putRecord")) assertEquals(0, server.countOf("com.atproto.repo.createRecord")) } @Test fun withNeitherProfileABareOneIsMadeAtSelfAndIndexed() = runTest { profileReads(grain = null, bsky = null) server.on("com.atproto.repo.createRecord", """{"uri":"at://did:plc:me/social.grain.actor.profile/self","cid":"c2"}""") val seeded = ProfileSeed.seed(client, auth) assertEquals(Seeded.Made("at://did:plc:me/social.grain.actor.profile/self"), seeded) val sent = server.lastCall("com.atproto.repo.createRecord").json assertEquals("self", sent["rkey"]!!.jsonPrimitive.content) assertEquals(ProfileSeed.GRAIN, sent["collection"]!!.jsonPrimitive.content) val record = sent["record"]!!.jsonObject assertEquals(setOf("\$type", "createdAt"), record.keys) assertEquals(ProfileSeed.GRAIN, record["\$type"]!!.jsonPrimitive.content) assertTrue(record["createdAt"]!!.jsonPrimitive.content.endsWith("Z")) assertEquals(0, server.countOf("com.atproto.repo.putRecord")) assertTrue(server.lastCall("social.grain.sync.indexRecords").body.contains("social.grain.actor.profile/self")) } @Test fun withNoBlueskyProfileAnUnnamedGrainProfileIsLeftAlone() = runTest { profileReads( grain = """{"uri":"at://did:plc:me/social.grain.actor.profile/self","cid":"c1",""" + """"value":{"createdAt":"2025-01-01T00:00:00.000Z"}}""", bsky = null, ) assertEquals(Seeded.NoBluesky, ProfileSeed.seed(client, auth)) assertEquals(0, server.countOf("com.atproto.repo.putRecord")) assertEquals(0, server.countOf("com.atproto.repo.createRecord")) } @Test fun withNoGrainProfileOneIsMadeAtSelfAndIndexed() = runTest { profileReads(grain = null, bsky = BSKY) server.on("com.atproto.repo.createRecord", """{"uri":"at://did:plc:me/social.grain.actor.profile/self","cid":"c2"}""") val seeded = ProfileSeed.seed(client, auth) assertEquals(Seeded.Made("at://did:plc:me/social.grain.actor.profile/self"), seeded) val sent = server.lastCall("com.atproto.repo.createRecord").json assertEquals("self", sent["rkey"]!!.jsonPrimitive.content) assertEquals(ProfileSeed.GRAIN, sent["collection"]!!.jsonPrimitive.content) val record = sent["record"]!!.jsonObject assertEquals(ProfileSeed.GRAIN, record["\$type"]!!.jsonPrimitive.content) assertEquals("Alice", record["displayName"]!!.jsonPrimitive.content) assertNull("An empty description is left out", record["description"]) assertNull("Only the name, description and avatar are taken", record["banner"]) assertTrue(record["createdAt"]!!.jsonPrimitive.content.endsWith("Z")) assertEquals(0, server.countOf("com.atproto.repo.putRecord")) assertTrue(server.lastCall("social.grain.sync.indexRecords").body.contains("social.grain.actor.profile/self")) } @Test fun anUnnamedGrainProfileIsPutOverOnItsCidKeepingWhenItWasMade() = runTest { profileReads( grain = """{"uri":"at://did:plc:me/social.grain.actor.profile/self","cid":"c1",""" + """"value":{"displayName":"","createdAt":"2025-01-01T00:00:00.000Z","description":"old"}}""", bsky = BSKY, ) val seeded = ProfileSeed.seed(client, auth) assertTrue(seeded is Seeded.Put) val sent = server.lastCall("com.atproto.repo.putRecord").json assertEquals("c1", sent["swapRecord"]!!.jsonPrimitive.content) assertEquals("self", sent["rkey"]!!.jsonPrimitive.content) val record = sent["record"]!!.jsonObject assertEquals("2025-01-01T00:00:00.000Z", record["createdAt"]!!.jsonPrimitive.content) assertNull("The Bluesky profile's empty description does not keep the old one", record["description"]) assertEquals(0, server.countOf("com.atproto.repo.createRecord")) } @Test fun theAvatarIsTheBlueskyBlobAsItIs() = runTest { profileReads(grain = null, bsky = BSKY) ProfileSeed.seed(client, auth) val record = server.lastCall("com.atproto.repo.createRecord").json["record"]!!.jsonObject assertEquals( kotlinx.serialization.json.Json.parseToJsonElement(avatar), record["avatar"], ) } private val BSKY: String get() = """{"uri":"at://did:plc:me/app.bsky.actor.profile/self","cid":"b1",""" + """"value":{"displayName":"Alice","description":"","avatar":$avatar,"banner":$avatar}}""" /** Answer the PDS's profile reads: [grain] and [bsky] as getRecord answers, or not found. */ private fun profileReads(grain: String?, bsky: String?) { server.answer("com.atproto.repo.getRecord") { call -> val record = if (call.params["collection"] == ProfileSeed.GRAIN) grain else bsky if (record != null) 200 to record else 400 to notFound } } }