50c0ec176c
* 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>
473 lines
13 KiB
TypeScript
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,
|
|
}),
|
|
),
|
|
)
|
|
})
|
|
})
|
|
})
|