atproto/packages/bsky/tests/views/actor-search.test.ts
Daniel Holmgren b15dec2f4f
Atproto sync package (#2752)
* first pass/port

* reworking

* authenticated commit parsing

* authenticate identity evts

* some testing

* tidy & add firehose to queue

* error handling

* fix test

* refactor sync queue + some tests

* fix race in sync queue

* rm firehose from syncqueue

* add tests for queue utils

* README

* lint readme

* filter before parsing

* pr feedback

* small fix

* changesets

* fix type

* Rework dataplane subscription (#2766)

* working sync package into appview subscription

* add restart method to subscription for tests

* fix another test

* tidy subscription utils/files

* remove dupe property

* tidy after merge

* fix start cursor on subscription

* tweak process full subscription logic

* fixes
2024-09-04 20:18:16 -05:00

257 lines
7.6 KiB
TypeScript

import { AtpAgent } from '@atproto/api'
import { wait } from '@atproto/common'
import { TestNetwork, SeedClient, usersBulkSeed } from '@atproto/dev-env'
import { forSnapshot, paginateAll, stripViewer } from '../_util'
import { ids } from '../../src/lexicon/lexicons'
// @NOTE skipped to help with CI failures
// The search code is not used in production & we should switch it out for tests on the search proxy interface
describe.skip('pds actor search views', () => {
let network: TestNetwork
let agent: AtpAgent
let sc: SeedClient
let headers: { [s: string]: string }
beforeAll(async () => {
network = await TestNetwork.create({
dbPostgresSchema: 'bsky_views_actor_search',
})
agent = network.bsky.getClient()
sc = network.getSeedClient()
await wait(50) // allow pending sub to be established
await network.bsky.sub.destroy()
await usersBulkSeed(sc)
// Skip did/handle resolution for expediency
const { db } = network.bsky
const now = new Date().toISOString()
await db.db
.insertInto('actor')
.values(
Object.entries(sc.dids).map(([handle, did]) => ({
did,
handle,
indexedAt: now,
})),
)
.onConflict((oc) => oc.doNothing())
.execute()
// Process remaining profiles
await network.bsky.sub.restart()
await network.processAll(50000)
headers = await network.serviceHeaders(
Object.values(sc.dids)[0],
ids.AppBskyActorSearchActorsTypeahead,
)
})
afterAll(async () => {
await network.close()
})
it('typeahead gives relevant results', async () => {
const result = await agent.api.app.bsky.actor.searchActorsTypeahead(
{ term: 'car' },
{ headers },
)
const handles = result.data.actors.map((u) => u.handle)
const shouldContain = [
'cara-wiegand69.test',
'eudora-dietrich4.test', // Carol Littel
'shane-torphy52.test', // Sadie Carter
'aliya-hodkiewicz.test', // Carlton Abernathy IV
'carlos6.test',
'carolina-mcderm77.test',
]
shouldContain.forEach((handle) => expect(handles).toContain(handle))
expect(handles).toContain('cayla-marquardt39.test') // Fuzzy match
const shouldNotContain = [
'sven70.test',
'hilario84.test',
'santa-hermann78.test',
'dylan61.test',
'preston-harris.test',
'loyce95.test',
'melyna-zboncak.test',
]
shouldNotContain.forEach((handle) => expect(handles).not.toContain(handle))
const sorted = result.data.actors.sort((a, b) =>
a.handle > b.handle ? 1 : -1,
)
expect(forSnapshot(sorted)).toMatchSnapshot()
})
it('typeahead gives empty result set when provided empty term', async () => {
const result = await agent.api.app.bsky.actor.searchActorsTypeahead(
{ term: '' },
{ headers },
)
expect(result.data.actors).toEqual([])
})
it('typeahead applies limit', async () => {
const full = await agent.api.app.bsky.actor.searchActorsTypeahead(
{ term: 'p' },
{ headers },
)
expect(full.data.actors.length).toBeGreaterThan(5)
const limited = await agent.api.app.bsky.actor.searchActorsTypeahead(
{ term: 'p', limit: 5 },
{ headers },
)
// @NOTE it's expected that searchActorsTypeahead doesn't have stable pagination
const limitedIndexInFull = limited.data.actors.map((needle) => {
return full.data.actors.findIndex(
(haystack) => needle.did === haystack.did,
)
})
// subset exists in full and is monotonic
expect(limitedIndexInFull.every((idx) => idx !== -1)).toEqual(true)
expect(limitedIndexInFull).toEqual(
[...limitedIndexInFull].sort((a, b) => a - b),
)
})
it('typeahead gives results unauthed', async () => {
const { data: authed } =
await agent.api.app.bsky.actor.searchActorsTypeahead(
{ term: 'car' },
{ headers },
)
const { data: unauthed } =
await agent.api.app.bsky.actor.searchActorsTypeahead({
term: 'car',
})
expect(unauthed.actors.length).toBeGreaterThan(0)
expect(unauthed.actors).toEqual(authed.actors.map(stripViewer))
})
it('search gives relevant results', async () => {
const result = await agent.api.app.bsky.actor.searchActors(
{ term: 'car' },
{ headers },
)
const handles = result.data.actors.map((u) => u.handle)
const shouldContain = [
'cara-wiegand69.test',
'eudora-dietrich4.test', // Carol Littel
'shane-torphy52.test', // Sadie Carter
'aliya-hodkiewicz.test', // Carlton Abernathy IV
'carlos6.test',
'carolina-mcderm77.test',
]
shouldContain.forEach((handle) => expect(handles).toContain(handle))
expect(handles).toContain('cayla-marquardt39.test') // Fuzzy match
const shouldNotContain = [
'sven70.test',
'hilario84.test',
'santa-hermann78.test',
'dylan61.test',
'preston-harris.test',
'loyce95.test',
'melyna-zboncak.test',
]
shouldNotContain.forEach((handle) => expect(handles).not.toContain(handle))
const sorted = result.data.actors.sort((a, b) =>
a.handle > b.handle ? 1 : -1,
)
expect(forSnapshot(sorted)).toMatchSnapshot()
})
it('search gives empty result set when provided empty term', async () => {
const result = await agent.api.app.bsky.actor.searchActors(
{ term: '' },
{ headers },
)
expect(result.data.actors).toEqual([])
})
it('paginates', async () => {
const results = (results) => results.flatMap((res) => res.users)
const paginator = async (cursor?: string) => {
const res = await agent.api.app.bsky.actor.searchActors(
{ term: 'p', cursor, limit: 3 },
{ headers },
)
return res.data
}
const paginatedAll = await paginateAll(paginator)
paginatedAll.forEach((res) =>
expect(res.actors.length).toBeLessThanOrEqual(3),
)
const full = await agent.api.app.bsky.actor.searchActors(
{ term: 'p' },
{ headers },
)
expect(full.data.actors.length).toBeGreaterThan(5)
const sortedFull = results([full.data]).sort((a, b) =>
a.handle > b.handle ? 1 : -1,
)
const sortedPaginated = results(paginatedAll).sort((a, b) =>
a.handle > b.handle ? 1 : -1,
)
expect(sortedPaginated).toEqual(sortedFull)
})
it('search handles bad input', async () => {
// Mostly for sqlite's benefit, since it uses LIKE and these are special characters that will
// get stripped. This input triggers a special case where there are no "safe" words for sqlite to search on.
const result = await agent.api.app.bsky.actor.searchActors(
{ term: ' % _ ' },
{ headers },
)
expect(result.data.actors).toEqual([])
})
it('search gives results unauthed', async () => {
const { data: authed } = await agent.api.app.bsky.actor.searchActors(
{ term: 'car' },
{ headers },
)
const { data: unauthed } = await agent.api.app.bsky.actor.searchActors({
term: 'car',
})
expect(unauthed.actors.length).toBeGreaterThan(0)
expect(unauthed.actors).toEqual(authed.actors.map(stripViewer))
})
it('search blocks by actor takedown', async () => {
await network.bsky.server.ctx.dataplane.takedownActor({
did: sc.dids['cara-wiegand69.test'],
})
const result = await agent.api.app.bsky.actor.searchActorsTypeahead(
{ term: 'car' },
{ headers },
)
const handles = result.data.actors.map((u) => u.handle)
expect(handles).toContain('carlos6.test')
expect(handles).toContain('carolina-mcderm77.test')
expect(handles).not.toContain('cara-wiegand69.test')
})
})