atproto/packages/bsky/tests/views/thread.test.ts
Daniel Holmgren 50c0ec176c
Service auth method binding (lxm) (#2663)
* add scopes to service auth impl

* add error to getServiceAuth

* send scoped tokens from pds

* clean up privileged access scopes & allow simple service auth tokens for app passwords

* integration into ozone

* fix up bsky tests

* cleanup xrpc-server tests

* fix up tests & types

* one more test

* fix read after write tests

* fix mod auth test

* convert scopes to be a single method name

* add scope check callback for auth verifier

* pds changes only

* fix feed generation tests

* use scope for ozone service profile

* dont verify scopes on pds yet

* tidy

* tidy imports

* changeset

* add tests

* tidy

* another changeset

* scope -> lxm

* tidy

* clean up scope references

* update nonce size

* pr feedback

* trim trailing slash

* nonce -> jti

* fix xrpc-server test

* allow service auth on uploadBlob

* fix build error

* changeset

* build, tidy

* xrpc-server: update lxm claim check error

* appview: temporarily permit labeler service calls to omit lxm claim

* xrpc-server: fix test

* changeset

* fix merged tests

---------

Co-authored-by: Devin Ivy <devinivy@gmail.com>
2024-08-18 15:46:07 -04:00

473 lines
13 KiB
TypeScript

import { AppBskyFeedGetPostThread, AtpAgent } from '@atproto/api'
import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env'
import {
assertIsThreadViewPost,
forSnapshot,
stripViewerFromThread,
} from '../_util'
import { ids } from '../../src/lexicon/lexicons'
describe('pds thread views', () => {
let network: TestNetwork
let agent: AtpAgent
let sc: SeedClient
// account dids, for convenience
let alice: string
let bob: string
let carol: string
beforeAll(async () => {
network = await TestNetwork.create({
dbPostgresSchema: 'bsky_views_thread',
})
agent = network.bsky.getClient()
sc = network.getSeedClient()
await basicSeed(sc)
alice = sc.dids.alice
bob = sc.dids.bob
carol = sc.dids.carol
})
beforeAll(async () => {
// Add a repost of a reply so that we can confirm myState in the thread
await sc.repost(bob, sc.replies[alice][0].ref)
await network.processAll()
})
afterAll(async () => {
await network.close()
})
it('fetches deep post thread', async () => {
const thread = await agent.api.app.bsky.feed.getPostThread(
{ uri: sc.posts[alice][1].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread.data.thread)).toMatchSnapshot()
})
it('fetches shallow post thread', async () => {
const thread = await agent.api.app.bsky.feed.getPostThread(
{ depth: 1, uri: sc.posts[alice][1].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread.data.thread)).toMatchSnapshot()
})
it('fetches thread with handle in uri', async () => {
const thread = await agent.api.app.bsky.feed.getPostThread(
{
depth: 1,
uri: sc.posts[alice][1].ref.uriStr.replace(
`at://${alice}`,
`at://${sc.accounts[alice].handle}`,
),
},
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread.data.thread)).toMatchSnapshot()
})
it('fetches ancestors', async () => {
const thread = await agent.api.app.bsky.feed.getPostThread(
{ depth: 1, uri: sc.replies[alice][0].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread.data.thread)).toMatchSnapshot()
})
it('fails for an unknown post', async () => {
const promise = agent.api.app.bsky.feed.getPostThread(
{ uri: 'at://did:example:fake/does.not.exist/self' },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
await expect(promise).rejects.toThrow(
AppBskyFeedGetPostThread.NotFoundError,
)
})
it('fetches post thread unauthed', async () => {
const { data: authed } = await agent.api.app.bsky.feed.getPostThread(
{ uri: sc.posts[alice][1].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
const { data: unauthed } = await agent.api.app.bsky.feed.getPostThread({
uri: sc.posts[alice][1].ref.uriStr,
})
expect(unauthed.thread).toEqual(stripViewerFromThread(authed.thread))
})
it('handles deleted posts correctly', async () => {
const alice = sc.dids.alice
const bob = sc.dids.bob
const indexes = {
aliceRoot: -1,
bobReply: -1,
aliceReplyReply: -1,
}
await sc.post(alice, 'Deletion thread')
indexes.aliceRoot = sc.posts[alice].length - 1
await sc.reply(
bob,
sc.posts[alice][indexes.aliceRoot].ref,
sc.posts[alice][indexes.aliceRoot].ref,
'Reply',
)
indexes.bobReply = sc.replies[bob].length - 1
await sc.reply(
alice,
sc.posts[alice][indexes.aliceRoot].ref,
sc.replies[bob][indexes.bobReply].ref,
'Reply reply',
)
indexes.aliceReplyReply = sc.replies[alice].length - 1
await network.processAll()
const thread1 = await agent.api.app.bsky.feed.getPostThread(
{ uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread1.data.thread)).toMatchSnapshot()
await sc.deletePost(bob, sc.replies[bob][indexes.bobReply].ref.uri)
await network.processAll()
const thread2 = await agent.api.app.bsky.feed.getPostThread(
{ uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread2.data.thread)).toMatchSnapshot()
const thread3 = await agent.api.app.bsky.feed.getPostThread(
{ uri: sc.replies[alice][indexes.aliceReplyReply].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread3.data.thread)).toMatchSnapshot()
})
it('omits parents and replies w/ different root than anchor post.', async () => {
const badRoot = sc.posts[alice][0]
const goodRoot = await sc.post(alice, 'good root')
const goodReply1 = await sc.reply(
alice,
goodRoot.ref,
goodRoot.ref,
'good reply 1',
)
const goodReply2 = await sc.reply(
alice,
goodRoot.ref,
goodReply1.ref,
'good reply 2',
)
const badReply = await sc.reply(
alice,
badRoot.ref,
goodReply1.ref,
'bad reply',
)
await network.processAll()
// good reply doesn't have replies w/ different root
const { data: goodReply1Thread } =
await agent.api.app.bsky.feed.getPostThread(
{ uri: goodReply1.ref.uriStr },
{
headers: await network.serviceHeaders(
alice,
ids.AppBskyFeedGetPostThread,
),
},
)
assertIsThreadViewPost(goodReply1Thread.thread)
assertIsThreadViewPost(goodReply1Thread.thread.parent)
expect(goodReply1Thread.thread.parent.post.uri).toEqual(goodRoot.ref.uriStr)
expect(
goodReply1Thread.thread.replies?.map((r) => {
assertIsThreadViewPost(r)
return r.post.uri
}),
).toEqual([
goodReply2.ref.uriStr, // does not contain badReply
])
expect(goodReply1Thread.thread.parent.replies).toBeUndefined()
// bad reply doesn't have a parent, which would have a different root
const { data: badReplyThread } =
await agent.api.app.bsky.feed.getPostThread(
{ uri: badReply.ref.uriStr },
{
headers: await network.serviceHeaders(
alice,
ids.AppBskyFeedGetPostThread,
),
},
)
assertIsThreadViewPost(badReplyThread.thread)
expect(badReplyThread.thread.parent).toBeUndefined() // is not goodReply1
})
it('reflects self-labels', async () => {
const { data: thread } = await agent.api.app.bsky.feed.getPostThread(
{ uri: sc.posts[alice][0].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
assertIsThreadViewPost(thread.thread)
const post = thread.thread.post
const postSelfLabels = post.labels
?.filter((label) => label.src === alice)
.map((label) => label.val)
expect(postSelfLabels).toEqual(['self-label'])
const authorSelfLabels = post.author.labels
?.filter((label) => label.src === alice)
.map((label) => label.val)
.sort()
expect(authorSelfLabels).toEqual(['self-label-a', 'self-label-b'])
})
describe('takedown', () => {
it('blocks post by actor', async () => {
await network.bsky.ctx.dataplane.takedownActor({
did: alice,
})
// Same as shallow post thread test, minus alice
const promise = agent.api.app.bsky.feed.getPostThread(
{ depth: 1, uri: sc.posts[alice][1].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
await expect(promise).rejects.toThrow(
AppBskyFeedGetPostThread.NotFoundError,
)
// Cleanup
await network.bsky.ctx.dataplane.untakedownActor({
did: alice,
})
})
it('blocks replies by actor', async () => {
await network.bsky.ctx.dataplane.takedownActor({
did: carol,
})
// Same as deep post thread test, minus carol
const thread = await agent.api.app.bsky.feed.getPostThread(
{ uri: sc.posts[alice][1].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread.data.thread)).toMatchSnapshot()
// Cleanup
await network.bsky.ctx.dataplane.untakedownActor({
did: carol,
})
})
it('blocks ancestors by actor', async () => {
await network.bsky.ctx.dataplane.takedownActor({
did: bob,
})
// Same as ancestor post thread test, minus bob
const thread = await agent.api.app.bsky.feed.getPostThread(
{ depth: 1, uri: sc.replies[alice][0].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread.data.thread)).toMatchSnapshot()
// Cleanup
await network.bsky.ctx.dataplane.untakedownActor({
did: bob,
})
})
it('blocks post by record', async () => {
const postRef = sc.posts[alice][1].ref
await network.bsky.ctx.dataplane.takedownRecord({
recordUri: postRef.uriStr,
})
const promise = agent.api.app.bsky.feed.getPostThread(
{ depth: 1, uri: postRef.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
await expect(promise).rejects.toThrow(
AppBskyFeedGetPostThread.NotFoundError,
)
// Cleanup
await network.bsky.ctx.dataplane.untakedownRecord({
recordUri: postRef.uriStr,
})
})
it('blocks ancestors by record', async () => {
const threadPreTakedown = await agent.api.app.bsky.feed.getPostThread(
{ depth: 1, uri: sc.replies[alice][0].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
const parent = threadPreTakedown.data.thread.parent?.['post']
await network.bsky.ctx.dataplane.takedownRecord({
recordUri: parent.uri,
})
// Same as ancestor post thread test, minus parent post
const thread = await agent.api.app.bsky.feed.getPostThread(
{ depth: 1, uri: sc.replies[alice][0].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread.data.thread)).toMatchSnapshot()
// Cleanup
await network.bsky.ctx.dataplane.untakedownRecord({
recordUri: parent.uri,
})
})
it('blocks replies by record', async () => {
const threadPreTakedown = await agent.api.app.bsky.feed.getPostThread(
{ uri: sc.posts[alice][1].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
const post1 = threadPreTakedown.data.thread.replies?.[0].post
const post2 = threadPreTakedown.data.thread.replies?.[1].replies[0].post
await Promise.all(
[post1, post2].map((post) =>
network.bsky.ctx.dataplane.takedownRecord({
recordUri: post.uri,
}),
),
)
// Same as deep post thread test, minus some replies
const thread = await agent.api.app.bsky.feed.getPostThread(
{ uri: sc.posts[alice][1].ref.uriStr },
{
headers: await network.serviceHeaders(
bob,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread.data.thread)).toMatchSnapshot()
// Cleanup
await Promise.all(
[post1, post2].map((post) =>
network.bsky.ctx.dataplane.untakedownRecord({
recordUri: post.uri,
}),
),
)
})
})
})