package social.grain import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put 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 androidx.test.ext.junit.runners.AndroidJUnit4 import social.grain.data.api.ApplyWrite import social.grain.data.api.BlockService import social.grain.data.api.CommentService import social.grain.data.api.FavoriteService import social.grain.data.api.FollowService import social.grain.data.api.GalleryService import social.grain.data.api.PinnedFeed import social.grain.data.api.ReportSubject import social.grain.data.api.StoryService import social.grain.data.api.applyWrites import social.grain.data.api.createReport import social.grain.data.api.createRecord import social.grain.data.api.deleteAccount import social.grain.data.api.deleteGallery import social.grain.data.api.deleteRecord import social.grain.data.api.describeLabels import social.grain.data.api.getActorFavorites import social.grain.data.api.getActorProfile import social.grain.data.api.getBlocks import social.grain.data.api.getCameras import social.grain.data.api.getCommentThread import social.grain.data.api.getFeed import social.grain.data.api.getFollowers import social.grain.data.api.getFollowing import social.grain.data.api.getGallery import social.grain.data.api.getGalleryFavorites import social.grain.data.api.getKnownFollowers import social.grain.data.api.getLocations import social.grain.data.api.getMutes import social.grain.data.api.getNotifications import social.grain.data.api.getPreferences import social.grain.data.api.getRecord import social.grain.data.api.getStories import social.grain.data.api.getStory import social.grain.data.api.getStoryArchive import social.grain.data.api.getStoryAuthors import social.grain.data.api.markNotificationsSeen import social.grain.data.api.muteActor import social.grain.data.api.putPreference import social.grain.data.api.putRecord import social.grain.data.api.registerPushToken import social.grain.data.api.searchActorsTypeahead import social.grain.data.api.searchGalleries import social.grain.data.api.searchProfiles import social.grain.data.api.unmuteActor import social.grain.data.api.unregisterPushToken import social.grain.data.model.BlobRef import social.grain.support.Fixtures import social.grain.support.StubServer import social.grain.support.json import social.grain.support.testAuth import social.grain.support.xrpc /** * Every call the app can make to the appview, exercised against a stub. * * These functions are one-liners each, which is exactly why they are worth * pinning: a query parameter named wrong or a body key spelled differently * compiles fine and fails only as an empty screen against the real server. The * assertions are on the wire format — NSID, method, parameters, body — because * that is the whole content of the layer. */ @RunWith(AndroidJUnit4::class) class EndpointsTest { private val server = StubServer() private val client = server.xrpc() private val auth = testAuth() private fun body(nsid: String): JsonObject = server.lastCall(nsid).json // MARK: - Feed @Test fun getFeedSendsEveryOptionalFilterItWasGiven() = runTest { server.on( "dev.hatk.getFeed", """{"items":[${json(Fixtures.gallery())}],"cursor":"page2"}""", ) val response = client.getFeed( feed = "camera", limit = 15, cursor = "page1", actor = "did:plc:alice", camera = "Fujifilm X100V", location = "8928308280fffff", name = "Mission District, San Francisco, US", tag = "fog", ) assertEquals(1, response.items?.size) assertEquals("page2", response.cursor) val call = server.lastCall("dev.hatk.getFeed") assertEquals("GET", call.method) assertEquals( mapOf( "feed" to "camera", "limit" to "15", "cursor" to "page1", "actor" to "did:plc:alice", "camera" to "Fujifilm X100V", "location" to "8928308280fffff", "name" to "Mission District, San Francisco, US", "tag" to "fog", ), call.params, ) } @Test fun getFeedOmitsFiltersThatWereNotSet() = runTest { server.on("dev.hatk.getFeed", """{"items":[]}""") client.getFeed(feed = "recent") assertEquals( mapOf("feed" to "recent", "limit" to "30"), server.lastCall("dev.hatk.getFeed").params, ) } @Test fun getGalleryAsksForOneGalleryByUri() = runTest { val gallery = Fixtures.gallery() server.on("social.grain.unspecced.getGallery", """{"gallery":${json(gallery)}}""") val response = client.getGallery(gallery.uri) assertEquals(gallery.uri, response.gallery.uri) assertEquals( mapOf("gallery" to gallery.uri), server.lastCall("social.grain.unspecced.getGallery").params, ) } @Test fun getCommentThreadPagesBySubject() = runTest { server.on( "social.grain.unspecced.getCommentThread", """{"comments":[${json(Fixtures.comment())}],"totalCount":9}""", ) val response = client.getCommentThread("at://x/gallery/1", limit = 5, cursor = "c") assertEquals(9, response.totalCount) assertEquals( mapOf("subject" to "at://x/gallery/1", "limit" to "5", "cursor" to "c"), server.lastCall("social.grain.unspecced.getCommentThread").params, ) } @Test fun getActorFavoritesReadsBackAFeedPage() = runTest { server.on("social.grain.unspecced.getActorFavorites", """{"items":[],"cursor":null}""") client.getActorFavorites("did:plc:alice", limit = 7) assertEquals( mapOf("actor" to "did:plc:alice", "limit" to "7"), server.lastCall("social.grain.unspecced.getActorFavorites").params, ) } @Test fun getPreferencesDecodesPinnedFeeds() = runTest { server.on( "dev.hatk.getPreferences", """{"preferences":{"pinnedFeeds":[{"id":"camera:Leica M6","label":"Leica M6",""" + """"type":"camera","path":"/cameras/leica"}],"includeExif":true}}""", ) val preferences = client.getPreferences().preferences assertEquals(true, preferences.includeExif) val pin = preferences.pinnedFeeds!!.single() assertEquals("camera", pin.feedName) assertEquals("Leica M6", pin.feedValue) } @Test fun searchActorsTypeaheadRunsUnauthenticated() = runTest { server.on( "social.grain.unspecced.searchActorsTypeahead", """{"actors":[{"did":"did:plc:alice","handle":"alice.grain.social"}]}""", ) val response = client.searchActorsTypeahead("ali", limit = 3) assertEquals("alice.grain.social", response.actors.single().handle) val call = server.lastCall("social.grain.unspecced.searchActorsTypeahead") assertEquals(mapOf("q" to "ali", "limit" to "3"), call.params) assertNull("Typeahead must work before there is a session", call.authorization) } // MARK: - Notifications @Test fun getNotificationsSendsCountOnlyOnlyWhenAsked() = runTest { server.on( "social.grain.unspecced.getNotifications", """{"notifications":[],"unseenCount":4}""", ) client.getNotifications(limit = 1, countOnly = true, auth = auth) assertEquals( mapOf("limit" to "1", "countOnly" to "true"), server.lastCall("social.grain.unspecced.getNotifications").params, ) client.getNotifications(limit = 100, cursor = "c", auth = auth) assertEquals( mapOf("limit" to "100", "cursor" to "c"), server.lastCall("social.grain.unspecced.getNotifications").params, ) } @Test fun markingNotificationsSeenWritesATimestampPreference() = runTest { client.markNotificationsSeen(auth) val sent = body("dev.hatk.putPreference") assertEquals("lastSeenNotifications", sent["key"]!!.jsonPrimitive.content) assertTrue( "The value should be an ISO timestamp", sent["value"]!!.jsonPrimitive.content.endsWith("Z"), ) } // MARK: - Stories @Test fun storyEndpointsAddressStoriesByActorAndUri() = runTest { server.on( "social.grain.unspecced.getStories", """{"stories":[${json(Fixtures.story())}]}""", ) server.on("social.grain.unspecced.getStory", """{"story":${json(Fixtures.story())}}""") server.on("social.grain.unspecced.getStoryArchive", """{"stories":[],"cursor":"c2"}""") server.on( "social.grain.unspecced.getStoryAuthors", """{"authors":[${json(Fixtures.storyAuthor())}]}""", ) assertEquals(1, client.getStories("did:plc:alice").stories.size) assertEquals( mapOf("actor" to "did:plc:alice"), server.lastCall("social.grain.unspecced.getStories").params, ) assertEquals("story1", client.getStory("at://x/story/1").story!!.rkey) assertEquals( mapOf("story" to "at://x/story/1"), server.lastCall("social.grain.unspecced.getStory").params, ) assertEquals("c2", client.getStoryArchive("did:plc:alice", limit = 4, cursor = "c1").cursor) assertEquals( mapOf("actor" to "did:plc:alice", "limit" to "4", "cursor" to "c1"), server.lastCall("social.grain.unspecced.getStoryArchive").params, ) assertEquals(1, client.getStoryAuthors().authors.size) assertTrue(server.lastCall("social.grain.unspecced.getStoryAuthors").params.isEmpty()) } // MARK: - Search @Test fun searchGalleriesCarriesTheFuzzyFlag() = runTest { server.on("social.grain.unspecced.searchGalleries", """{"items":[],"cursor":null}""") client.searchGalleries("fog", limit = 12, cursor = "c", fuzzy = false) assertEquals( mapOf("q" to "fog", "limit" to "12", "fuzzy" to "false", "cursor" to "c"), server.lastCall("social.grain.unspecced.searchGalleries").params, ) } @Test fun searchProfilesDecodesActorListItems() = runTest { server.on( "social.grain.unspecced.searchProfiles", """{"items":[{"did":"did:plc:bob","handle":"bob.grain.social"}]}""", ) val response = client.searchProfiles("bob") assertEquals("did:plc:bob", response.items!!.single().did) assertEquals( mapOf("q" to "bob", "limit" to "30"), server.lastCall("social.grain.unspecced.searchProfiles").params, ) } // MARK: - Discovery @Test fun getCamerasDecodesTheIndexWithItsThumbnails() = runTest { server.on( "social.grain.unspecced.getCameras", """{"cameras":[{"camera":"Leica M6","photoCount":120,"thumbs":["https://cdn.grain.test/thumb/1.jpg"]}]}""", ) val response = client.getCameras() val camera = response.cameras!!.single() assertEquals("Leica M6", camera.camera) assertEquals(120, camera.photoCount) assertEquals(listOf("https://cdn.grain.test/thumb/1.jpg"), camera.thumbs) assertTrue(server.lastCall("social.grain.unspecced.getCameras").params.isEmpty()) } @Test fun getLocationsAsksForPinsOnlyWhenToldTo() = runTest { server.on( "social.grain.unspecced.getLocations", """{"locations":[{"name":"Lisbon, PT","h3Index":"85390cb7fffffff","galleryCount":3}]}""", ) val full = client.getLocations() assertEquals("Lisbon, PT", full.locations!!.single().name) assertEquals("85390cb7fffffff", full.locations!!.single().h3Index) assertTrue(full.locations!!.single().thumbs.isEmpty()) assertTrue(server.lastCall("social.grain.unspecced.getLocations").params.isEmpty()) client.getLocations(pins = true) assertEquals( mapOf("pins" to "true"), server.lastCall("social.grain.unspecced.getLocations").params, ) } // MARK: - Profile @Test fun getActorProfileSendsTheViewerOnlyWhenSignedIn() = runTest { server.on( "social.grain.unspecced.getActorProfile", json(Fixtures.profileDetailed(following = "at://follow/1")), ) val signedOut = client.getActorProfile("alice.grain.social") assertEquals("did:plc:alice", signedOut.did) assertEquals( mapOf("actor" to "alice.grain.social"), server.lastCall("social.grain.unspecced.getActorProfile").params, ) client.getActorProfile("alice.grain.social", viewer = "did:plc:me", auth = auth) assertEquals( mapOf("actor" to "alice.grain.social", "viewer" to "did:plc:me"), server.lastCall("social.grain.unspecced.getActorProfile").params, ) } @Test fun followerAndFollowingListsPaginate() = runTest { server.on("social.grain.unspecced.getFollowers", """{"totalCount":3,"items":[]}""") server.on("social.grain.unspecced.getFollowing", """{"totalCount":4,"items":[]}""") assertEquals(3, client.getFollowers("did:plc:alice", viewer = "did:plc:me", cursor = "c").totalCount) assertEquals( mapOf( "actor" to "did:plc:alice", "limit" to "50", "viewer" to "did:plc:me", "cursor" to "c", ), server.lastCall("social.grain.unspecced.getFollowers").params, ) assertEquals(4, client.getFollowing("did:plc:alice", limit = 10).totalCount) assertEquals( mapOf("actor" to "did:plc:alice", "limit" to "10"), server.lastCall("social.grain.unspecced.getFollowing").params, ) } @Test fun galleryFavoritesAndKnownFollowersAddressTheirSubject() = runTest { server.on("social.grain.unspecced.getGalleryFavorites", """{"totalCount":8,"items":[]}""") server.on("social.grain.unspecced.getKnownFollowers", """{"items":[]}""") assertEquals(8, client.getGalleryFavorites("at://x/gallery/1").totalCount) assertEquals( mapOf("gallery" to "at://x/gallery/1", "limit" to "50"), server.lastCall("social.grain.unspecced.getGalleryFavorites").params, ) client.getKnownFollowers("did:plc:alice", "did:plc:me") assertEquals( mapOf("actor" to "did:plc:alice", "viewer" to "did:plc:me"), server.lastCall("social.grain.unspecced.getKnownFollowers").params, ) } // MARK: - Records @Test fun getRecordAddressesARecordByUri() = runTest { server.on("dev.hatk.getRecord", """{"uri":"at://x/y/1","cid":"cid1","record":{"a":1}}""") val response = client.getRecord("at://x/y/1") assertEquals("cid1", response.cid) assertEquals(mapOf("uri" to "at://x/y/1"), server.lastCall("dev.hatk.getRecord").params) } @Test fun createRecordPostsCollectionRepoAndRecord() = runTest { server.on("dev.hatk.createRecord", """{"uri":"at://new","cid":"cid-new"}""") val response = client.createRecord( collection = "social.grain.favorite", repo = "did:plc:me", record = buildJsonObject { put("subject", "at://x") }, auth = auth, ) assertEquals("at://new", response.uri) val sent = body("dev.hatk.createRecord") assertEquals("social.grain.favorite", sent["collection"]!!.jsonPrimitive.content) assertEquals("did:plc:me", sent["repo"]!!.jsonPrimitive.content) assertEquals("at://x", sent["record"]!!.jsonObject["subject"]!!.jsonPrimitive.content) assertEquals("POST", server.lastCall("dev.hatk.createRecord").method) } @Test fun putRecordCarriesTheRkeyAndOmitsAnAbsentRepo() = runTest { server.on("dev.hatk.putRecord", """{"uri":"at://put","cid":"cid"}""") client.putRecord("social.grain.actor.profile", "self", buildJsonObject { }, auth = auth) assertNull(body("dev.hatk.putRecord")["repo"]) client.putRecord( "social.grain.actor.profile", "self", buildJsonObject { }, repo = "did:plc:me", auth = auth, ) val sent = body("dev.hatk.putRecord") assertEquals("self", sent["rkey"]!!.jsonPrimitive.content) assertEquals("did:plc:me", sent["repo"]!!.jsonPrimitive.content) } @Test fun deleteRecordNamesTheCollectionAndRkey() = runTest { client.deleteRecord("social.grain.comment", "abc", auth) val sent = body("dev.hatk.deleteRecord") assertEquals("social.grain.comment", sent["collection"]!!.jsonPrimitive.content) assertEquals("abc", sent["rkey"]!!.jsonPrimitive.content) } @Test fun applyWritesBatchesEveryCreateIntoOneCommit() = runTest { server.on("dev.hatk.applyWrites", """{"results":[{"uri":"at://a"},{"uri":"at://b"}]}""") val response = client.applyWrites( listOf( ApplyWrite("social.grain.photo", "p1", buildJsonObject { put("alt", "one") }), ApplyWrite("social.grain.gallery", "g1", buildJsonObject { put("title", "two") }), ), auth, ) assertEquals(2, response.results?.size) val writes = body("dev.hatk.applyWrites")["writes"]!!.jsonArray assertEquals(2, writes.size) val first = writes[0].jsonObject assertEquals("dev.hatk.applyWrites#create", first["\$type"]!!.jsonPrimitive.content) assertEquals("social.grain.photo", first["collection"]!!.jsonPrimitive.content) assertEquals("p1", first["rkey"]!!.jsonPrimitive.content) assertEquals("one", first["value"]!!.jsonObject["alt"]!!.jsonPrimitive.content) } @Test fun deletingAGalleryGoesThroughTheUnspeccedEndpoint() = runTest { client.deleteGallery("g1", auth) assertEquals( "g1", body("social.grain.unspecced.deleteGallery")["rkey"]!!.jsonPrimitive.content, ) } // MARK: - Moderation @Test fun describeLabelsTakesNoParameters() = runTest { server.on( "dev.hatk.describeLabels", """{"definitions":[{"identifier":"spoiler","blurs":"none"}]}""", ) assertEquals("spoiler", client.describeLabels().definitions!!.single().identifier) assertTrue(server.lastCall("dev.hatk.describeLabels").params.isEmpty()) } @Test fun reportingARecordSendsAStrongRef() = runTest { server.on("dev.hatk.createReport", """{"id":42}""") val response = client.createReport( ReportSubject.Record("at://x/gallery/1", "cid1"), label = "spam", reason = "Repeated posts", auth = auth, ) assertEquals(42, response.id) val sent = body("dev.hatk.createReport") val subject = sent["subject"]!!.jsonObject assertEquals("com.atproto.repo.strongRef", subject["\$type"]!!.jsonPrimitive.content) assertEquals("at://x/gallery/1", subject["uri"]!!.jsonPrimitive.content) assertEquals("cid1", subject["cid"]!!.jsonPrimitive.content) assertEquals("spam", sent["label"]!!.jsonPrimitive.content) assertEquals("Repeated posts", sent["reason"]!!.jsonPrimitive.content) } @Test fun reportingAnAccountSendsABareDidAndNoReason() = runTest { server.on("dev.hatk.createReport", """{"id":7}""") client.createReport(ReportSubject.Account("did:plc:bob"), label = "spam", auth = auth) val sent = body("dev.hatk.createReport") assertEquals("did:plc:bob", sent["subject"]!!.jsonObject["did"]!!.jsonPrimitive.content) assertNull(sent["reason"]) } // MARK: - Push @Test fun pushTokensRegisterAndUnregisterForThisPlatform() = runTest { client.registerPushToken("fcm-token", auth) val registered = body("dev.hatk.push.registerToken") assertEquals("fcm-token", registered["token"]!!.jsonPrimitive.content) assertEquals("fcm", registered["platform"]!!.jsonPrimitive.content) client.unregisterPushToken("fcm-token", auth) assertEquals( "fcm-token", body("dev.hatk.push.unregisterToken")["token"]!!.jsonPrimitive.content, ) } // MARK: - Moderation lists @Test fun blockAndMuteListsAreReadWholeAndAlwaysAuthenticated() = runTest { server.on( "social.grain.unspecced.getBlocks", """{"items":[{"did":"did:plc:bob","blockUri":"at://block/1"}]}""", ) server.on("social.grain.unspecced.getMutes", """{"items":[{"did":"did:plc:carol"}]}""") assertEquals("at://block/1", client.getBlocks(auth).items!!.single().blockUri) assertEquals("did:plc:carol", client.getMutes(auth).items!!.single().did) assertTrue( server.lastCall("social.grain.unspecced.getBlocks") .authorization!!.startsWith("DPoP "), ) } @Test fun mutingAndUnmutingNameTheActor() = runTest { client.muteActor("did:plc:bob", auth) assertEquals( "did:plc:bob", body("social.grain.graph.muteActor")["actor"]!!.jsonPrimitive.content, ) client.unmuteActor("did:plc:bob", auth) assertEquals( "did:plc:bob", body("social.grain.graph.unmuteActor")["actor"]!!.jsonPrimitive.content, ) } @Test fun blockServiceCreatesARecordAndDeletesByRkey() = runTest { server.on("dev.hatk.createRecord", """{"uri":"at://did:plc:me/block/xyz"}""") BlockService.block("did:plc:bob", "did:plc:me", client, auth) val record = body("dev.hatk.createRecord")["record"]!!.jsonObject assertEquals("did:plc:bob", record["subject"]!!.jsonPrimitive.content) assertTrue(record.containsKey("createdAt")) BlockService.unblock("at://did:plc:me/social.grain.graph.block/xyz", client, auth) val deleted = body("dev.hatk.deleteRecord") assertEquals(BlockService.COLLECTION, deleted["collection"]!!.jsonPrimitive.content) assertEquals("xyz", deleted["rkey"]!!.jsonPrimitive.content) } @Test fun preferencesAndAccountDeletionPostTheirOwnShapes() = runTest { client.putPreference("includeExif", value = false, auth = auth) val sent = body("dev.hatk.putPreference") assertEquals("includeExif", sent["key"]!!.jsonPrimitive.content) assertEquals("false", sent["value"]!!.jsonPrimitive.content) client.deleteAccount(auth) assertTrue(body("social.grain.unspecced.deleteAccount").isEmpty()) } // MARK: - Services @Test fun favoriteAndFollowServicesWriteTheirOwnCollections() = runTest { server.on("dev.hatk.createRecord", """{"uri":"at://fav"}""") FavoriteService.create("at://x/gallery/1", "did:plc:me", client, auth) assertEquals( FavoriteService.COLLECTION, body("dev.hatk.createRecord")["collection"]!!.jsonPrimitive.content, ) FavoriteService.delete("at://did:plc:me/social.grain.favorite/f1", client, auth) assertEquals("f1", body("dev.hatk.deleteRecord")["rkey"]!!.jsonPrimitive.content) FollowService.create("did:plc:bob", "did:plc:me", client, auth) val follow = body("dev.hatk.createRecord") assertEquals(FollowService.COLLECTION, follow["collection"]!!.jsonPrimitive.content) assertEquals( "did:plc:bob", follow["record"]!!.jsonObject["subject"]!!.jsonPrimitive.content, ) FollowService.delete("at://did:plc:me/social.grain.graph.follow/fl1", client, auth) assertEquals("fl1", body("dev.hatk.deleteRecord")["rkey"]!!.jsonPrimitive.content) } @Test fun galleryServiceDeletesByTheRkeyInTheUri() = runTest { GalleryService.delete("at://did:plc:me/social.grain.gallery/g9", client, auth) assertEquals( "g9", body("social.grain.unspecced.deleteGallery")["rkey"]!!.jsonPrimitive.content, ) } @Test fun commentServiceCarriesTheReplyTargetOnlyWhenReplying() = runTest { server.on("dev.hatk.createRecord", """{"uri":"at://c"}""") CommentService.create("at://x/gallery/1", "Nice", "did:plc:me", client = client, auth = auth) val top = body("dev.hatk.createRecord")["record"]!!.jsonObject assertEquals("Nice", top["text"]!!.jsonPrimitive.content) assertEquals("at://x/gallery/1", top["subject"]!!.jsonPrimitive.content) assertNull(top["replyTo"]) CommentService.create( "at://x/gallery/1", "Agreed", "did:plc:me", replyTo = "at://x/comment/1", client = client, auth = auth, ) assertEquals( "at://x/comment/1", body("dev.hatk.createRecord")["record"]!! .jsonObject["replyTo"]!!.jsonPrimitive.content, ) CommentService.delete("at://did:plc:me/social.grain.comment/c1", client, auth) assertEquals("c1", body("dev.hatk.deleteRecord")["rkey"]!!.jsonPrimitive.content) } @Test fun storyServiceBuildsTheMediaBlobAndAspectRatio() = runTest { server.on("dev.hatk.createRecord", """{"uri":"at://s"}""") StoryService.create( blob = BlobRef( type = "blob", ref = BlobRef.BlobLink("bafyabc"), mimeType = "image/jpeg", size = 1234, ), width = 1080, height = 1920, repo = "did:plc:me", client = client, auth = auth, ) val record = body("dev.hatk.createRecord")["record"]!!.jsonObject val media = record["media"]!!.jsonObject assertEquals("bafyabc", media["ref"]!!.jsonObject["\$link"]!!.jsonPrimitive.content) assertEquals("image/jpeg", media["mimeType"]!!.jsonPrimitive.content) assertEquals("1234", media["size"]!!.jsonPrimitive.content) assertEquals("1080", record["aspectRatio"]!!.jsonObject["width"]!!.jsonPrimitive.content) assertNull(record["location"]) assertNull(record["labels"]) } @Test fun storyServiceAttachesLocationAndSelfLabelsWhenGiven() = runTest { server.on("dev.hatk.createRecord", """{"uri":"at://s"}""") StoryService.create( blob = BlobRef(ref = BlobRef.BlobLink("bafy")), width = 9, height = 16, repo = "did:plc:me", labels = listOf("nudity"), locationCell = "8928308280fffff", locationName = "Mission District", address = buildJsonObject { put("country", "US") }, client = client, auth = auth, ) val record = body("dev.hatk.createRecord")["record"]!!.jsonObject val location = record["location"]!!.jsonObject assertEquals("8928308280fffff", location["value"]!!.jsonPrimitive.content) assertEquals("Mission District", location["name"]!!.jsonPrimitive.content) assertEquals("US", record["address"]!!.jsonObject["country"]!!.jsonPrimitive.content) val labels = record["labels"]!!.jsonObject assertEquals( "com.atproto.label.defs#selfLabels", labels["\$type"]!!.jsonPrimitive.content, ) assertEquals( "nudity", labels["values"]!!.jsonArray.single().jsonObject["val"]!!.jsonPrimitive.content, ) } @Test fun storyServiceDeletesByRkey() = runTest { StoryService.delete("at://did:plc:me/social.grain.story/s1", client, auth) assertEquals("s1", body("dev.hatk.deleteRecord")["rkey"]!!.jsonPrimitive.content) } @Test fun storyServiceFallsBackWhenTheBlobIsMissingItsFields() = runTest { server.on("dev.hatk.createRecord", """{"uri":"at://s"}""") StoryService.create( blob = BlobRef(), width = 1, height = 1, repo = "did:plc:me", client = client, auth = auth, ) val media = body("dev.hatk.createRecord")["record"]!!.jsonObject["media"]!!.jsonObject assertEquals("blob", media["\$type"]!!.jsonPrimitive.content) assertEquals("", media["ref"]!!.jsonObject["\$link"]!!.jsonPrimitive.content) assertEquals("image/jpeg", media["mimeType"]!!.jsonPrimitive.content) assertEquals("0", media["size"]!!.jsonPrimitive.content) } // MARK: - Pinned feeds @Test fun aPinnedFeedNamesItselfByTypeOnlyWhenItIsACompoundId() = runTest { val camera = PinnedFeed("camera:Leica M6", "Leica M6", "camera", "/c") assertEquals("camera", camera.feedName) assertEquals("Leica M6", camera.feedValue) val recent = PinnedFeed.defaults.first() assertEquals("recent", recent.feedName) assertNull(recent.feedValue) val hashtag = PinnedFeed("hashtag:fog", "#fog", "hashtag", "/t/fog") assertEquals("hashtag", hashtag.feedName) assertEquals("fog", hashtag.feedValue) } }