atproto/packages/bsky/tests/views/feed-view-post.test.ts
Eric Bailey 922b2e82ac
Compute 3p blocks for ReplyRef.root (#2721)
* Add grandparent to reply ref

* Hydrate grandparent on reply ref

* Changeset

* Check for 3p blocks on ReplyRef.root

* Revert "Add grandparent to reply ref"

This reverts commit 73012b67262eced0f626c2f05592aaea655d6f79.

* Revert "Changeset"

This reverts commit 4264b085ae7c4cb9bd833528f354973be1a05305.

* Remove redundant hydration

* More tests

* Check for 3p blocks between grandparent and root

* Just kidding, ensure we only use first slice to compute from

* Clarify block relationship

* Add anti-test

* Rename test file to be more precise

* Make sure we get child->root block relationships

* Rename postBlocks.reply to parent

* Ensure first slice relationships are still correct if replyRef data is outside first page of results

* Remove unnecessary check

* Format

* Use latest APIs

* Prevent [creator, creator] checks
2024-08-20 14:00:07 -05:00

502 lines
15 KiB
TypeScript

import { AtpAgent, AppBskyFeedDefs, AtUri } from '@atproto/api'
import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env'
import { ids } from '../../src/lexicon/lexicons'
/**
* The frontend computes feed slices for display using at-most one
* `FeedViewPost` slice. If that slice results in an "orphaned" thread e.g.
* parent or root is blocked, that slice is tossed out and the next
* `FeedViewPost` slice is considered. That process continues until a
* contiguous `root -> child` slice can be found.
*
* For the tests below, we test with up to 4 slices: one root and up to 3
* replies. Some tests focus on the first slice, and others ensure that at
* least one contiguous slice is returned.
*/
const LIMIT = 4
describe('pds thread views', () => {
let network: TestNetwork
let agent: AtpAgent
let pdsAgent: AtpAgent
let sc: SeedClient
// account dids, for convenience
let alice: string
let bob: string
let carol: string
let dan: string
beforeAll(async () => {
network = await TestNetwork.create({
dbPostgresSchema: 'bsky_views_feed_view_post',
})
agent = network.bsky.getClient()
pdsAgent = network.pds.getClient()
sc = network.getSeedClient()
await basicSeed(sc)
alice = sc.dids.alice
bob = sc.dids.bob
carol = sc.dids.carol
dan = sc.dids.dan
await sc.follow(carol, alice)
await sc.follow(carol, bob)
await sc.follow(carol, dan)
await sc.follow(dan, alice)
await sc.follow(dan, bob)
await sc.follow(dan, carol)
await network.processAll()
})
afterAll(async () => {
await network.close()
})
it(`[A] -> [B], A blocks B, viewed as C`, async () => {
const A = await sc.post(alice, `A`)
await network.processAll()
const B = await sc.reply(bob, A.ref, A.ref, `B`)
await network.processAll()
const block = await pdsAgent.api.app.bsky.graph.block.create(
{ repo: alice },
{ createdAt: new Date().toISOString(), subject: bob },
sc.getHeaders(alice),
)
await network.processAll()
const timeline = await agent.api.app.bsky.feed.getTimeline(
{ limit: LIMIT },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyFeedGetTimeline,
),
},
)
const sliceA = timeline.data.feed.find((f) => f.post.uri === A.ref.uriStr)
const sliceB = timeline.data.feed.find((f) => f.post.uri === B.ref.uriStr)
expect(sliceA).toBeDefined()
expect(sliceB).toBeDefined()
if (!sliceA || !sliceB) {
throw new Error('sliceA or sliceB is undefined')
}
expect(AppBskyFeedDefs.isBlockedPost(sliceB.reply?.parent)).toBe(true)
expect(AppBskyFeedDefs.isBlockedPost(sliceB.reply?.root)).toBe(true)
await pdsAgent.api.app.bsky.graph.block.delete(
{ repo: alice, rkey: new AtUri(block.uri).rkey },
sc.getHeaders(alice),
)
})
it(`[A] -> [B] -> [C], A blocks B, viewed as C`, async () => {
const A = await sc.post(alice, `A`)
await network.processAll()
const B = await sc.reply(bob, A.ref, A.ref, `B`)
await network.processAll()
const C = await sc.reply(carol, A.ref, B.ref, `C`)
const block = await pdsAgent.api.app.bsky.graph.block.create(
{ repo: alice },
{ createdAt: new Date().toISOString(), subject: bob },
sc.getHeaders(alice),
)
await network.processAll()
const timeline = await agent.api.app.bsky.feed.getTimeline(
{ limit: LIMIT },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyFeedGetTimeline,
),
},
)
const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr)
expect(sliceC).toBeDefined()
expect(sliceC?.reply).toBeDefined()
if (!sliceC || !sliceC.reply) {
throw new Error('sliceC is undefined')
}
expect(sliceC.reply.parent.uri).toEqual(B.ref.uriStr)
expect(sliceC.reply.root.uri).toEqual(A.ref.uriStr)
expect(AppBskyFeedDefs.isPostView(sliceC.reply.parent)).toBe(true)
expect(AppBskyFeedDefs.isBlockedPost(sliceC.reply.root)).toBe(true)
await pdsAgent.api.app.bsky.graph.block.delete(
{ repo: alice, rkey: new AtUri(block.uri).rkey },
sc.getHeaders(alice),
)
})
it(`[A] -> [B] -> [C], C blocks A, viewed as C`, async () => {
const A = await sc.post(alice, `A`)
await network.processAll()
const B = await sc.reply(bob, A.ref, A.ref, `B`)
await network.processAll()
const C = await sc.reply(carol, A.ref, B.ref, `C`)
const block = await pdsAgent.api.app.bsky.graph.block.create(
{ repo: carol },
{ createdAt: new Date().toISOString(), subject: alice },
sc.getHeaders(carol),
)
await network.processAll()
const timeline = await agent.api.app.bsky.feed.getTimeline(
// make sure we process all slices in this test
{ limit: LIMIT },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyFeedGetTimeline,
),
},
)
const sliceB = timeline.data.feed.find((f) => f.post.uri === B.ref.uriStr)
const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr)
expect(sliceB).toBeUndefined()
expect(sliceC).toBeUndefined()
await pdsAgent.api.app.bsky.graph.block.delete(
{ repo: carol, rkey: new AtUri(block.uri).rkey },
sc.getHeaders(carol),
)
})
it(`[A] -> [B] -> [C], C blocks B, viewed as C`, async () => {
const A = await sc.post(alice, `A`)
await network.processAll()
const B = await sc.reply(bob, A.ref, A.ref, `B`)
await network.processAll()
const C = await sc.reply(carol, A.ref, B.ref, `C`)
const block = await pdsAgent.api.app.bsky.graph.block.create(
{ repo: carol },
{ createdAt: new Date().toISOString(), subject: bob },
sc.getHeaders(carol),
)
await network.processAll()
const timeline = await agent.api.app.bsky.feed.getTimeline(
// make sure we process all slices in this test
{ limit: LIMIT },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyFeedGetTimeline,
),
},
)
const sliceA = timeline.data.feed.find((f) => f.post.uri === A.ref.uriStr)
const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr)
expect(sliceA).toBeDefined()
expect(sliceC).toBeUndefined()
await pdsAgent.api.app.bsky.graph.block.delete(
{ repo: carol, rkey: new AtUri(block.uri).rkey },
sc.getHeaders(carol),
)
})
it(`[A] -> [B] -> [C] -> [D], A blocks C, viewed as C`, async () => {
const A = await sc.post(alice, `A`)
await network.processAll()
const B = await sc.reply(bob, A.ref, A.ref, `B`)
await network.processAll()
const C = await sc.reply(carol, A.ref, B.ref, `C`)
await network.processAll()
const D = await sc.reply(dan, A.ref, C.ref, `D`)
const block = await pdsAgent.api.app.bsky.graph.block.create(
{ repo: alice },
{ createdAt: new Date().toISOString(), subject: carol },
sc.getHeaders(alice),
)
await network.processAll()
const timeline = await agent.api.app.bsky.feed.getTimeline(
{ limit: LIMIT },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyFeedGetTimeline,
),
},
)
const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)
expect(sliceD).toBeDefined()
expect(sliceD?.reply).toBeDefined()
if (!sliceD || !sliceD.reply) {
throw new Error('sliceD is undefined')
}
expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)
expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)
expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true)
expect(AppBskyFeedDefs.isBlockedPost(sliceD.reply.root)).toBe(true)
await pdsAgent.api.app.bsky.graph.block.delete(
{ repo: alice, rkey: new AtUri(block.uri).rkey },
sc.getHeaders(alice),
)
})
it(`[A] -> [B] -> [C] -> [D], A blocks C, viewed as D`, async () => {
const A = await sc.post(alice, `A`)
await network.processAll()
const B = await sc.reply(bob, A.ref, A.ref, `B`)
await network.processAll()
const C = await sc.reply(carol, A.ref, B.ref, `C`)
await network.processAll()
const D = await sc.reply(dan, A.ref, C.ref, `D`)
const block = await pdsAgent.api.app.bsky.graph.block.create(
{ repo: alice },
{ createdAt: new Date().toISOString(), subject: carol },
sc.getHeaders(alice),
)
await network.processAll()
const timeline = await agent.api.app.bsky.feed.getTimeline(
{ limit: LIMIT },
{
headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),
},
)
const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)
expect(sliceD).toBeDefined()
expect(sliceD?.reply).toBeDefined()
if (!sliceD || !sliceD.reply) {
throw new Error('sliceD is undefined')
}
expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)
expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)
expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true)
expect(AppBskyFeedDefs.isBlockedPost(sliceD.reply.root)).toBe(true)
await pdsAgent.api.app.bsky.graph.block.delete(
{ repo: alice, rkey: new AtUri(block.uri).rkey },
sc.getHeaders(alice),
)
})
it(`[A] -> [B] -> [C] -> [D], A blocks B, viewed as C`, async () => {
const A = await sc.post(alice, `A`)
await network.processAll()
const B = await sc.reply(bob, A.ref, A.ref, `B`)
await network.processAll()
const C = await sc.reply(carol, A.ref, B.ref, `C`)
await network.processAll()
const D = await sc.reply(dan, A.ref, C.ref, `D`)
const block = await pdsAgent.api.app.bsky.graph.block.create(
{ repo: alice },
{ createdAt: new Date().toISOString(), subject: bob },
sc.getHeaders(alice),
)
await network.processAll()
const timeline = await agent.api.app.bsky.feed.getTimeline(
{ limit: LIMIT },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyFeedGetTimeline,
),
},
)
const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)
expect(sliceD).toBeDefined()
expect(sliceD?.reply).toBeDefined()
if (!sliceD || !sliceD.reply) {
throw new Error('sliceD is undefined')
}
expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)
expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)
expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true)
/*
* We don't walk the reply ancestors past whats available in the ReplyRef
*/
expect(AppBskyFeedDefs.isPostView(sliceD.reply.root)).toBe(true)
await pdsAgent.api.app.bsky.graph.block.delete(
{ repo: alice, rkey: new AtUri(block.uri).rkey },
sc.getHeaders(alice),
)
})
it(`[A] -> [B] -> [C] -> [D], B blocks C, viewed as D`, async () => {
const A = await sc.post(alice, `A`)
await network.processAll()
const B = await sc.reply(bob, A.ref, A.ref, `B`)
await network.processAll()
const C = await sc.reply(carol, A.ref, B.ref, `C`)
await network.processAll()
const D = await sc.reply(dan, A.ref, C.ref, `D`)
const block = await pdsAgent.api.app.bsky.graph.block.create(
{ repo: bob },
{ createdAt: new Date().toISOString(), subject: carol },
sc.getHeaders(bob),
)
await network.processAll()
const timeline = await agent.api.app.bsky.feed.getTimeline(
{ limit: LIMIT },
{
headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),
},
)
const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)
expect(sliceD).toBeDefined()
expect(sliceD?.reply).toBeDefined()
if (!sliceD || !sliceD.reply) {
throw new Error('sliceD is undefined')
}
expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)
expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)
expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true)
/*
* We don't walk the reply ancestors past whats available in the ReplyRef
*/
expect(AppBskyFeedDefs.isPostView(sliceD.reply.root)).toBe(true)
await pdsAgent.api.app.bsky.graph.block.delete(
{ repo: bob, rkey: new AtUri(block.uri).rkey },
sc.getHeaders(bob),
)
})
it(`[A] -> [B] -> [C] -> [D], A blocks D, viewed as C`, async () => {
const A = await sc.post(alice, `A`)
await network.processAll()
const B = await sc.reply(bob, A.ref, A.ref, `B`)
await network.processAll()
const C = await sc.reply(carol, A.ref, B.ref, `C`)
await network.processAll()
const D = await sc.reply(dan, A.ref, C.ref, `D`)
const block = await pdsAgent.api.app.bsky.graph.block.create(
{ repo: alice },
{ createdAt: new Date().toISOString(), subject: dan },
sc.getHeaders(alice),
)
await network.processAll()
const timeline = await agent.api.app.bsky.feed.getTimeline(
{ limit: LIMIT },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyFeedGetTimeline,
),
},
)
const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)
const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr)
expect(sliceD).toBeDefined()
expect(sliceC).toBeDefined()
if (!sliceD || !sliceC) {
throw new Error('sliceD or sliceC is undefined')
}
expect(AppBskyFeedDefs.isBlockedPost(sliceD.reply?.root)).toBe(true)
expect(AppBskyFeedDefs.isPostView(sliceC.reply?.parent)).toBe(true)
expect(AppBskyFeedDefs.isPostView(sliceC.reply?.root)).toBe(true)
await pdsAgent.api.app.bsky.graph.block.delete(
{ repo: alice, rkey: new AtUri(block.uri).rkey },
sc.getHeaders(alice),
)
})
it(`[A] -> [B] -> [C] -> [D], A blocks C, viewed as D, A/B/C are outside first page`, async () => {
const A = await sc.post(alice, `A`)
await network.processAll()
const B = await sc.reply(bob, A.ref, A.ref, `B`)
await network.processAll()
const C = await sc.reply(carol, A.ref, B.ref, `C`)
await network.processAll()
// push A/B/C to send page of results
await sc.post(alice, `Aa`)
await sc.post(alice, `Ab`)
await sc.post(alice, `Ac`)
await sc.post(alice, `Ad`)
await network.processAll()
const D = await sc.reply(dan, A.ref, C.ref, `D`)
const block = await pdsAgent.api.app.bsky.graph.block.create(
{ repo: alice },
{ createdAt: new Date().toISOString(), subject: carol },
sc.getHeaders(alice),
)
await network.processAll()
const timeline = await agent.api.app.bsky.feed.getTimeline(
{ limit: LIMIT },
{
headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),
},
)
const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)
const sliceA = timeline.data.feed.find((f) => f.post.uri === A.ref.uriStr)
expect(sliceD).toBeDefined()
expect(sliceD?.reply).toBeDefined()
// not in first page of results
expect(sliceA).toBeUndefined()
if (!sliceD || !sliceD.reply) {
throw new Error('sliceD is undefined')
}
expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)
expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)
expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true)
expect(AppBskyFeedDefs.isBlockedPost(sliceD.reply.root)).toBe(true)
await pdsAgent.api.app.bsky.graph.block.delete(
{ repo: alice, rkey: new AtUri(block.uri).rkey },
sc.getHeaders(alice),
)
})
})