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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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
}
}
}