atproto/packages/bsky/tests/views/block-lists.test.ts
rafael 638f5a8312
Fix avatar path resolution in dev-env (#3266)
* Fix avatar path resolution in dev-env

* changeset

* extract dev-env assets to dedicated folder

* add comment

* fix fmt
2024-12-19 11:25:41 -03:00

663 lines
19 KiB
TypeScript

import { AtpAgent, AtUri } from '@atproto/api'
import { TestNetwork, SeedClient, RecordRef, basicSeed } from '@atproto/dev-env'
import { forSnapshot } from '../_util'
import { ids } from '../../src/lexicon/lexicons'
describe('pds views with blocking from block lists', () => {
let network: TestNetwork
let agent: AtpAgent
let pdsAgent: AtpAgent
let sc: SeedClient
let aliceReplyToDan: { ref: RecordRef }
let alice: string
let bob: string
let carol: string
let dan: string
beforeAll(async () => {
network = await TestNetwork.create({
dbPostgresSchema: 'views_block_lists',
})
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
// add follows to ensure blocks work even w follows
await sc.follow(carol, dan)
await sc.follow(dan, carol)
aliceReplyToDan = await sc.reply(
alice,
sc.posts[dan][0].ref,
sc.posts[dan][0].ref,
'alice replies to dan',
)
await network.processAll()
})
afterAll(async () => {
await network.close()
})
let listUri: string
it('creates a list with some items', async () => {
const avatar = await sc.uploadFile(
alice,
'../dev-env/assets/key-portrait-small.jpg',
'image/jpeg',
)
// alice creates block list with bob & carol that dan uses
const list = await pdsAgent.api.app.bsky.graph.list.create(
{ repo: alice },
{
name: 'alice blocks',
purpose: 'app.bsky.graph.defs#modlist',
description: 'big list of blocks',
avatar: avatar.image,
createdAt: new Date().toISOString(),
},
sc.getHeaders(alice),
)
listUri = list.uri
await pdsAgent.api.app.bsky.graph.listitem.create(
{ repo: alice },
{
subject: sc.dids.bob,
list: list.uri,
createdAt: new Date().toISOString(),
},
sc.getHeaders(alice),
)
await pdsAgent.api.app.bsky.graph.listitem.create(
{ repo: alice },
{
subject: sc.dids.carol,
list: list.uri,
createdAt: new Date().toISOString(),
},
sc.getHeaders(alice),
)
await pdsAgent.api.app.bsky.graph.listitem.create(
{ repo: alice },
{
subject: sc.dids.dan,
list: list.uri,
createdAt: new Date().toISOString(),
},
sc.getHeaders(alice),
)
await network.processAll()
})
it('uses a list for blocks', async () => {
await pdsAgent.api.app.bsky.graph.listblock.create(
{ repo: dan },
{
subject: listUri,
createdAt: new Date().toISOString(),
},
sc.getHeaders(dan),
)
await network.processAll()
})
it('blocks thread post', async () => {
const { carol, dan } = sc.dids
const { data: threadAlice } = await agent.api.app.bsky.feed.getPostThread(
{ depth: 1, uri: sc.posts[carol][0].ref.uriStr },
{
headers: await network.serviceHeaders(
dan,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(threadAlice.thread).toEqual(
expect.objectContaining({
$type: 'app.bsky.feed.defs#blockedPost',
uri: sc.posts[carol][0].ref.uriStr,
blocked: true,
}),
)
const { data: threadCarol } = await agent.api.app.bsky.feed.getPostThread(
{ depth: 1, uri: sc.posts[dan][0].ref.uriStr },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(threadCarol.thread).toEqual(
expect.objectContaining({
$type: 'app.bsky.feed.defs#blockedPost',
uri: sc.posts[dan][0].ref.uriStr,
blocked: true,
}),
)
})
it('blocks thread reply', async () => {
// Contains reply by carol
const { data: thread } = await agent.api.app.bsky.feed.getPostThread(
{ depth: 1, uri: sc.posts[alice][1].ref.uriStr },
{
headers: await network.serviceHeaders(
dan,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread)).toMatchSnapshot()
})
it('blocks thread parent', async () => {
// Parent is a post by dan
const { data: thread } = await agent.api.app.bsky.feed.getPostThread(
{ depth: 1, uri: aliceReplyToDan.ref.uriStr },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread)).toMatchSnapshot()
})
it('blocks record embeds', async () => {
// Contains a deep embed of carol's post, blocked by dan
const { data: thread } = await agent.api.app.bsky.feed.getPostThread(
{ depth: 0, uri: sc.posts[alice][2].ref.uriStr },
{
headers: await network.serviceHeaders(
dan,
ids.AppBskyFeedGetPostThread,
),
},
)
expect(forSnapshot(thread)).toMatchSnapshot()
})
it('errors on getting author feed', async () => {
const attempt1 = agent.api.app.bsky.feed.getAuthorFeed(
{ actor: carol },
{
headers: await network.serviceHeaders(
dan,
ids.AppBskyFeedGetAuthorFeed,
),
},
)
await expect(attempt1).rejects.toMatchObject({
error: 'BlockedActor',
})
const attempt2 = agent.api.app.bsky.feed.getAuthorFeed(
{ actor: dan },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyFeedGetAuthorFeed,
),
},
)
await expect(attempt2).rejects.toMatchObject({
error: 'BlockedByActor',
})
})
it('strips blocked users out of getTimeline', async () => {
const resCarol = await agent.api.app.bsky.feed.getTimeline(
{ limit: 100 },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyFeedGetTimeline,
),
},
)
expect(
resCarol.data.feed.some((post) => post.post.author.did === dan),
).toBeFalsy()
const resDan = await agent.api.app.bsky.feed.getTimeline(
{ limit: 100 },
{
headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),
},
)
expect(
resDan.data.feed.some((post) =>
[bob, carol].includes(post.post.author.did),
),
).toBeFalsy()
})
it('returns block status on getProfile', async () => {
const resCarol = await agent.api.app.bsky.actor.getProfile(
{ actor: dan },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyActorGetProfile,
),
},
)
expect(resCarol.data.viewer?.blocking).toBeUndefined()
expect(resCarol.data.viewer?.blockingByList).toBeUndefined()
expect(resCarol.data.viewer?.blockedBy).toBe(true)
const resDan = await agent.api.app.bsky.actor.getProfile(
{ actor: carol },
{
headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile),
},
)
expect(resDan.data.viewer?.blocking).toBeDefined()
expect(resDan.data.viewer?.blockingByList?.uri).toEqual(
resDan.data.viewer?.blocking,
)
expect(resDan.data.viewer?.blockedBy).toBe(false)
})
it('returns block status on getProfiles', async () => {
const resCarol = await agent.api.app.bsky.actor.getProfiles(
{ actors: [alice, dan] },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyActorGetProfiles,
),
},
)
expect(resCarol.data.profiles[0].viewer?.blocking).toBeUndefined()
expect(resCarol.data.profiles[0].viewer?.blockingByList).toBeUndefined()
expect(resCarol.data.profiles[0].viewer?.blockedBy).toBe(false)
expect(resCarol.data.profiles[1].viewer?.blocking).toBeUndefined()
expect(resCarol.data.profiles[1].viewer?.blockingByList).toBeUndefined()
expect(resCarol.data.profiles[1].viewer?.blockedBy).toBe(true)
const resDan = await agent.api.app.bsky.actor.getProfiles(
{ actors: [alice, carol] },
{
headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfiles),
},
)
expect(resDan.data.profiles[0].viewer?.blocking).toBeUndefined()
expect(resDan.data.profiles[0].viewer?.blockingByList).toBeUndefined()
expect(resDan.data.profiles[0].viewer?.blockedBy).toBe(false)
expect(resDan.data.profiles[1].viewer?.blocking).toBeDefined()
expect(resDan.data.profiles[1].viewer?.blockingByList?.uri).toEqual(
resDan.data.profiles[1].viewer?.blocking,
)
expect(resDan.data.profiles[1].viewer?.blockedBy).toBe(false)
})
it('ignores self-blocks', async () => {
const res = await agent.api.app.bsky.actor.getProfile(
{ actor: dan }, // dan subscribes to list that contains himself
{
headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile),
},
)
expect(res.data.viewer?.blocking).toBeUndefined()
expect(res.data.viewer?.blockingByList).toBeUndefined()
expect(res.data.viewer?.blockedBy).toBe(false)
})
it('does not return notifs for blocked accounts', async () => {
const resCarol = await agent.api.app.bsky.notification.listNotifications(
{
limit: 100,
},
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyNotificationListNotifications,
),
},
)
expect(
resCarol.data.notifications.some((notif) => notif.author.did === dan),
).toBeFalsy()
const resDan = await agent.api.app.bsky.notification.listNotifications(
{
limit: 100,
},
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyNotificationListNotifications,
),
},
)
expect(
resDan.data.notifications.some((notif) => notif.author.did === carol),
).toBeFalsy()
})
it('does not return blocked accounts in actor search', async () => {
const resCarol = await agent.api.app.bsky.actor.searchActors(
{
term: 'dan.test',
},
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyActorSearchActors,
),
},
)
expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy()
const resDan = await agent.api.app.bsky.actor.searchActors(
{
term: 'carol.test',
},
{
headers: await network.serviceHeaders(
dan,
ids.AppBskyActorSearchActors,
),
},
)
expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy()
})
it('does not return blocked accounts in actor search typeahead', async () => {
const resCarol = await agent.api.app.bsky.actor.searchActorsTypeahead(
{
term: 'dan.tes',
},
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyActorSearchActorsTypeahead,
),
},
)
expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy()
const resDan = await agent.api.app.bsky.actor.searchActorsTypeahead(
{
term: 'carol.tes',
},
{
headers: await network.serviceHeaders(
dan,
ids.AppBskyActorSearchActorsTypeahead,
),
},
)
expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy()
})
it('does return blocked accounts in actor search typeahead when term is exact handle', async () => {
const resCarol = await agent.api.app.bsky.actor.searchActorsTypeahead(
{
term: 'dan.test',
},
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyActorSearchActorsTypeahead,
),
},
)
expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeTruthy()
const resDan = await agent.api.app.bsky.actor.searchActorsTypeahead(
{
term: 'carol.test',
},
{
headers: await network.serviceHeaders(
dan,
ids.AppBskyActorSearchActorsTypeahead,
),
},
)
expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeTruthy()
})
it('does not return blocked accounts in get suggestions', async () => {
// unfollow so they _would_ show up in suggestions if not for block
await sc.unfollow(carol, dan)
await sc.unfollow(dan, carol)
await network.processAll()
const resCarol = await agent.api.app.bsky.actor.getSuggestions(
{
limit: 100,
},
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyActorGetSuggestions,
),
},
)
expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy()
const resDan = await agent.api.app.bsky.actor.getSuggestions(
{
limit: 100,
},
{
headers: await network.serviceHeaders(
dan,
ids.AppBskyActorGetSuggestions,
),
},
)
expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy()
})
it('returns the contents of a list', async () => {
const res = await agent.api.app.bsky.graph.getList(
{ list: listUri },
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },
)
expect(forSnapshot(res.data)).toMatchSnapshot()
})
it('paginates getList', async () => {
const full = await agent.api.app.bsky.graph.getList(
{ list: listUri },
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },
)
const first = await agent.api.app.bsky.graph.getList(
{ list: listUri, limit: 1 },
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },
)
const second = await agent.api.app.bsky.graph.getList(
{ list: listUri, cursor: first.data.cursor },
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },
)
const combined = [...first.data.items, ...second.data.items]
expect(combined).toEqual(full.data.items)
})
let otherListUri: string
it('returns lists associated with a user', async () => {
const listRes = await pdsAgent.api.app.bsky.graph.list.create(
{ repo: alice },
{
name: 'new list',
purpose: 'app.bsky.graph.defs#modlist',
description: 'blah blah',
createdAt: new Date().toISOString(),
},
sc.getHeaders(alice),
)
otherListUri = listRes.uri
await network.processAll()
const res = await agent.api.app.bsky.graph.getLists(
{ actor: alice },
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },
)
expect(forSnapshot(res.data)).toMatchSnapshot()
})
it('paginates getLists', async () => {
const full = await agent.api.app.bsky.graph.getLists(
{ actor: alice },
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },
)
const first = await agent.api.app.bsky.graph.getLists(
{ actor: alice, limit: 1 },
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },
)
const second = await agent.api.app.bsky.graph.getLists(
{ actor: alice, cursor: first.data.cursor },
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },
)
const combined = [...first.data.lists, ...second.data.lists]
expect(combined).toEqual(full.data.lists)
})
it('returns a users own list blocks', async () => {
await pdsAgent.api.app.bsky.graph.listblock.create(
{ repo: dan },
{
subject: otherListUri,
createdAt: new Date().toISOString(),
},
sc.getHeaders(dan),
)
await network.processAll()
const res = await agent.api.app.bsky.graph.getListBlocks(
{},
{
headers: await network.serviceHeaders(
dan,
ids.AppBskyGraphGetListBlocks,
),
},
)
expect(forSnapshot(res.data)).toMatchSnapshot()
})
it('paginates getListBlocks', async () => {
const full = await agent.api.app.bsky.graph.getListBlocks(
{},
{
headers: await network.serviceHeaders(
dan,
ids.AppBskyGraphGetListBlocks,
),
},
)
const first = await agent.api.app.bsky.graph.getListBlocks(
{ limit: 1 },
{
headers: await network.serviceHeaders(
dan,
ids.AppBskyGraphGetListBlocks,
),
},
)
const second = await agent.api.app.bsky.graph.getListBlocks(
{ cursor: first.data.cursor },
{
headers: await network.serviceHeaders(
dan,
ids.AppBskyGraphGetListBlocks,
),
},
)
const combined = [...first.data.lists, ...second.data.lists]
expect(combined).toEqual(full.data.lists)
})
it('does not apply "curate" blocklists', async () => {
const parsedUri = new AtUri(listUri)
await pdsAgent.api.com.atproto.repo.putRecord(
{
repo: parsedUri.hostname,
collection: parsedUri.collection,
rkey: parsedUri.rkey,
record: {
name: 'curate list',
purpose: 'app.bsky.graph.defs#curatelist',
createdAt: new Date().toISOString(),
},
},
{ headers: sc.getHeaders(alice), encoding: 'application/json' },
)
await network.processAll()
const resCarol = await agent.api.app.bsky.feed.getTimeline(
{ limit: 100 },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyFeedGetTimeline,
),
},
)
expect(
resCarol.data.feed.some((post) => post.post.author.did === dan),
).toBeTruthy()
const resDan = await agent.api.app.bsky.feed.getTimeline(
{ limit: 100 },
{
headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),
},
)
expect(
resDan.data.feed.some((post) =>
[bob, carol].includes(post.post.author.did),
),
).toBeTruthy()
})
it('does not apply deleted blocklists (whose items are still around)', async () => {
const parsedUri = new AtUri(listUri)
await pdsAgent.api.app.bsky.graph.list.delete(
{
repo: parsedUri.hostname,
rkey: parsedUri.rkey,
},
sc.getHeaders(alice),
)
await network.processAll()
const resCarol = await agent.api.app.bsky.feed.getTimeline(
{ limit: 100 },
{
headers: await network.serviceHeaders(
carol,
ids.AppBskyFeedGetTimeline,
),
},
)
expect(
resCarol.data.feed.some((post) => post.post.author.did === dan),
).toBeTruthy()
const resDan = await agent.api.app.bsky.feed.getTimeline(
{ limit: 100 },
{
headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),
},
)
expect(
resDan.data.feed.some((post) =>
[bob, carol].includes(post.post.author.did),
),
).toBeTruthy()
})
})