* refactor(crypto): remove circular dependency * refactor(crypto): expose compress/decompress as part of the DidKeyPlugin interface * fix(crypto): remove import from private file * refactor: isolate tsconfig * fix: remove unused bench file * chore(repo): remove unused deps * fix(ozone): properly list dependencies * fix(services): do lint js files * fix(services/pds): remove unused deps * chore(pds): remove bench * chore(dev-env): remove unused deps * chore(api): remove bench * remove unused babel.config.js files * fix: remove .ts extension from import * fix(pds): remove imports of src files * fix(tsconfig): properly list all projects * fix(dev-env): remove imports of src files * fix(bsky): remove direct import to crypto src * fix(api): remove imports to api internals * chore(build): prevent bundling of built output * chore(dev): add "dev" script to build in watch mode * chore(deps): move ts-node dependency where it is actually used * fix(deps): add dev-env as project dependency * fix(xrpc-server): properly type kexicon * fix(bsky): improve typings * fix(pds): fully type formatRecordEmbedInternal return value * fix(repo): remove imports from @ipld/car/api * feat(dev-env): re-export BskyIngester * fix: properly lint & type jest config & test files * fix(ci): test after build * fix(types): use NodeJS.Timeout instead of NodeJS.Timer * fix(bsky): make types exportable * fix(ozone): make types exportable * fix(xrpc-server): make types exportable * fix(xprc-server): make code compliant with "node" types * fix(xrpc-server): avoid accessing properties of unknown * chore(deps): update @types/node * feat(tsconfig): narrow down available types depending on the package's target environment * fix(pds): remove unused prop * fix(bsync): Database's migrator not always initialized * fix(dev-env): remove unreachable code * fix(xrpc-server): remove unused import * fix(xrpc-server): mark header property as abstract * fix(pds): initialize LeakyTxPlugin's txOver property * fix(bsky): initialize LeakyTxPlugin's txOver property * fix(bsky): remove unused migrator from DatabaseCoordinator * fix(bsky): Properly initialize LabelService's cache property * fix(ozone): Database's migrator not initialized * fix(ozone): initialize LeakyTxPlugin's txOver property * fix(crypto): ignore unused variable error * feat(tsconfig): use stricter rules * feat(tsconfig): enable useDefineForClassFields * feat(xrpc-server): add support for brotli incoming payload * fix(xrpc-server): properly parse & process content-encoding * fix(common:stream): always call cb in _transform * tidy/fix tests and service entrypoints * Revert "fix(xrpc-server): properly parse & process content-encoding" This reverts commit 2b1c66e153820d3e128fc839fcc1834d52a66686. * Revert "feat(xrpc-server): add support for brotli incoming payload" This reverts commit e710c21e6118214ddf215b0515e68cb87299a952. * remove special node env for tests (defaults to jest val of "test") * kill mute sync handler on disconnect * work around connect-es bug w/ request aborts * style(crypto): rename imports from uint8arrays * fix update package-lock * fix lint * force hbs files to be bundled as cjs * fix: use concurrently instead of npm-run-all npm-run-all seems not to be maintained anymore. Additionally, concurrently better forwards signals to child processes. * remove concurrently alltogether * ignore sqlite files in services/pds * fix verify * fix verify * tidy, fix verify * fix blob diversion test * build rework changeset --------- Co-authored-by: Devin Ivy <devinivy@gmail.com>
711 lines
23 KiB
TypeScript
711 lines
23 KiB
TypeScript
import { sql } from 'kysely'
|
|
import { CID } from 'multiformats/cid'
|
|
import { cidForCbor, TID } from '@atproto/common'
|
|
import { repoPrepare } from '@atproto/pds'
|
|
import { WriteOpAction } from '@atproto/repo'
|
|
import { AtUri } from '@atproto/syntax'
|
|
import AtpAgent, {
|
|
AppBskyActorProfile,
|
|
AppBskyFeedPost,
|
|
AppBskyFeedLike,
|
|
AppBskyFeedRepost,
|
|
AppBskyGraphFollow,
|
|
} from '@atproto/api'
|
|
import { TestNetwork, SeedClient, usersSeed, basicSeed } from '@atproto/dev-env'
|
|
import { forSnapshot } from '../_util'
|
|
import { ids } from '../../src/lexicon/lexicons'
|
|
import { Database } from '../../src/data-plane/server/db'
|
|
|
|
describe('indexing', () => {
|
|
let network: TestNetwork
|
|
let agent: AtpAgent
|
|
let pdsAgent: AtpAgent
|
|
let sc: SeedClient
|
|
let db: Database
|
|
|
|
beforeAll(async () => {
|
|
network = await TestNetwork.create({
|
|
dbPostgresSchema: 'bsky_indexing',
|
|
})
|
|
agent = network.bsky.getClient()
|
|
pdsAgent = network.pds.getClient()
|
|
sc = network.getSeedClient()
|
|
db = network.bsky.db
|
|
await usersSeed(sc)
|
|
// Data in tests is not processed from subscription
|
|
await network.processAll()
|
|
await network.bsky.sub.destroy()
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await network.close()
|
|
})
|
|
|
|
it('indexes posts.', async () => {
|
|
const createdAt = new Date().toISOString()
|
|
const createRecord = await prepareCreate({
|
|
did: sc.dids.alice,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: {
|
|
$type: ids.AppBskyFeedPost,
|
|
text: '@bob.test how are you?',
|
|
facets: [
|
|
{
|
|
index: { byteStart: 0, byteEnd: 9 },
|
|
features: [
|
|
{
|
|
$type: `${ids.AppBskyRichtextFacet}#mention`,
|
|
did: sc.dids.bob,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
createdAt,
|
|
} as AppBskyFeedPost.Record,
|
|
})
|
|
const [uri] = createRecord
|
|
const updateRecord = await prepareUpdate({
|
|
did: sc.dids.alice,
|
|
collection: ids.AppBskyFeedPost,
|
|
rkey: uri.rkey,
|
|
record: {
|
|
$type: ids.AppBskyFeedPost,
|
|
text: '@carol.test how are you?',
|
|
facets: [
|
|
{
|
|
index: { byteStart: 0, byteEnd: 11 },
|
|
features: [
|
|
{
|
|
$type: `${ids.AppBskyRichtextFacet}#mention`,
|
|
did: sc.dids.carol,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
createdAt,
|
|
} as AppBskyFeedPost.Record,
|
|
})
|
|
const deleteRecord = prepareDelete({
|
|
did: sc.dids.alice,
|
|
collection: ids.AppBskyFeedPost,
|
|
rkey: uri.rkey,
|
|
})
|
|
|
|
// Create
|
|
await network.bsky.sub.indexingSvc.indexRecord(...createRecord)
|
|
|
|
const getAfterCreate = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: uri.toString() },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
expect(forSnapshot(getAfterCreate.data)).toMatchSnapshot()
|
|
const createNotifications = await getNotifications(db, uri)
|
|
|
|
// Update
|
|
await network.bsky.sub.indexingSvc.indexRecord(...updateRecord)
|
|
|
|
const getAfterUpdate = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: uri.toString() },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
expect(forSnapshot(getAfterUpdate.data)).toMatchSnapshot()
|
|
const updateNotifications = await getNotifications(db, uri)
|
|
|
|
// Delete
|
|
await network.bsky.sub.indexingSvc.deleteRecord(...deleteRecord)
|
|
|
|
const getAfterDelete = agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: uri.toString() },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
await expect(getAfterDelete).rejects.toThrow(/Post not found:/)
|
|
const deleteNotifications = await getNotifications(db, uri)
|
|
|
|
expect(
|
|
forSnapshot({
|
|
createNotifications,
|
|
updateNotifications,
|
|
deleteNotifications,
|
|
}),
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
it('indexes profiles.', async () => {
|
|
const createRecord = await prepareCreate({
|
|
did: sc.dids.dan,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: 'self',
|
|
record: {
|
|
$type: ids.AppBskyActorProfile,
|
|
displayName: 'dan',
|
|
} as AppBskyActorProfile.Record,
|
|
})
|
|
const [uri] = createRecord
|
|
const updateRecord = await prepareUpdate({
|
|
did: sc.dids.dan,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: uri.rkey,
|
|
record: {
|
|
$type: ids.AppBskyActorProfile,
|
|
displayName: 'danny',
|
|
} as AppBskyActorProfile.Record,
|
|
})
|
|
const deleteRecord = prepareDelete({
|
|
did: sc.dids.dan,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: uri.rkey,
|
|
})
|
|
|
|
// Create
|
|
await network.bsky.sub.indexingSvc.indexRecord(...createRecord)
|
|
|
|
const getAfterCreate = await agent.api.app.bsky.actor.getProfile(
|
|
{ actor: sc.dids.dan },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
expect(forSnapshot(getAfterCreate.data)).toMatchSnapshot()
|
|
|
|
// Update
|
|
await network.bsky.sub.indexingSvc.indexRecord(...updateRecord)
|
|
|
|
const getAfterUpdate = await agent.api.app.bsky.actor.getProfile(
|
|
{ actor: sc.dids.dan },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
expect(forSnapshot(getAfterUpdate.data)).toMatchSnapshot()
|
|
|
|
// Delete
|
|
await network.bsky.sub.indexingSvc.deleteRecord(...deleteRecord)
|
|
|
|
const getAfterDelete = await agent.api.app.bsky.actor.getProfile(
|
|
{ actor: sc.dids.dan },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
expect(forSnapshot(getAfterDelete.data)).toMatchSnapshot()
|
|
})
|
|
|
|
it('handles post aggregations out of order.', async () => {
|
|
const createdAt = new Date().toISOString()
|
|
const originalPost = await prepareCreate({
|
|
did: sc.dids.alice,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: {
|
|
$type: ids.AppBskyFeedPost,
|
|
text: 'original post',
|
|
createdAt,
|
|
} as AppBskyFeedPost.Record,
|
|
})
|
|
const originalPostRef = {
|
|
uri: originalPost[0].toString(),
|
|
cid: originalPost[1].toString(),
|
|
}
|
|
const reply = await prepareCreate({
|
|
did: sc.dids.bob,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: {
|
|
$type: ids.AppBskyFeedPost,
|
|
text: 'reply post',
|
|
reply: {
|
|
root: originalPostRef,
|
|
parent: originalPostRef,
|
|
},
|
|
createdAt,
|
|
} as AppBskyFeedPost.Record,
|
|
})
|
|
const like = await prepareCreate({
|
|
did: sc.dids.bob,
|
|
collection: ids.AppBskyFeedLike,
|
|
record: {
|
|
$type: ids.AppBskyFeedLike,
|
|
subject: originalPostRef,
|
|
createdAt,
|
|
} as AppBskyFeedLike.Record,
|
|
})
|
|
const repost = await prepareCreate({
|
|
did: sc.dids.bob,
|
|
collection: ids.AppBskyFeedRepost,
|
|
record: {
|
|
$type: ids.AppBskyFeedRepost,
|
|
subject: originalPostRef,
|
|
createdAt,
|
|
} as AppBskyFeedRepost.Record,
|
|
})
|
|
// reply, like, and repost indexed orior to the original post
|
|
await network.bsky.sub.indexingSvc.indexRecord(...reply)
|
|
await network.bsky.sub.indexingSvc.indexRecord(...like)
|
|
await network.bsky.sub.indexingSvc.indexRecord(...repost)
|
|
await network.bsky.sub.indexingSvc.indexRecord(...originalPost)
|
|
await network.bsky.sub.background.processAll()
|
|
const agg = await db.db
|
|
.selectFrom('post_agg')
|
|
.selectAll()
|
|
.where('uri', '=', originalPostRef.uri)
|
|
.executeTakeFirst()
|
|
expect(agg).toEqual({
|
|
uri: originalPostRef.uri,
|
|
replyCount: 1,
|
|
repostCount: 1,
|
|
likeCount: 1,
|
|
})
|
|
// Cleanup
|
|
const del = (uri: AtUri) => {
|
|
return prepareDelete({
|
|
did: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
}
|
|
await network.bsky.sub.indexingSvc.deleteRecord(...del(reply[0]))
|
|
await network.bsky.sub.indexingSvc.deleteRecord(...del(like[0]))
|
|
await network.bsky.sub.indexingSvc.deleteRecord(...del(repost[0]))
|
|
await network.bsky.sub.indexingSvc.deleteRecord(...del(originalPost[0]))
|
|
})
|
|
|
|
it('does not notify user of own like or repost', async () => {
|
|
const createdAt = new Date().toISOString()
|
|
|
|
const originalPost = await prepareCreate({
|
|
did: sc.dids.bob,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: {
|
|
$type: ids.AppBskyFeedPost,
|
|
text: 'original post',
|
|
createdAt,
|
|
} as AppBskyFeedPost.Record,
|
|
})
|
|
|
|
const originalPostRef = {
|
|
uri: originalPost[0].toString(),
|
|
cid: originalPost[1].toString(),
|
|
}
|
|
|
|
// own actions
|
|
const ownLike = await prepareCreate({
|
|
did: sc.dids.bob,
|
|
collection: ids.AppBskyFeedLike,
|
|
record: {
|
|
$type: ids.AppBskyFeedLike,
|
|
subject: originalPostRef,
|
|
createdAt,
|
|
} as AppBskyFeedLike.Record,
|
|
})
|
|
const ownRepost = await prepareCreate({
|
|
did: sc.dids.bob,
|
|
collection: ids.AppBskyFeedRepost,
|
|
record: {
|
|
$type: ids.AppBskyFeedRepost,
|
|
subject: originalPostRef,
|
|
createdAt,
|
|
} as AppBskyFeedRepost.Record,
|
|
})
|
|
|
|
// other actions
|
|
const aliceLike = await prepareCreate({
|
|
did: sc.dids.alice,
|
|
collection: ids.AppBskyFeedLike,
|
|
record: {
|
|
$type: ids.AppBskyFeedLike,
|
|
subject: originalPostRef,
|
|
createdAt,
|
|
} as AppBskyFeedLike.Record,
|
|
})
|
|
const aliceRepost = await prepareCreate({
|
|
did: sc.dids.alice,
|
|
collection: ids.AppBskyFeedRepost,
|
|
record: {
|
|
$type: ids.AppBskyFeedRepost,
|
|
subject: originalPostRef,
|
|
createdAt,
|
|
} as AppBskyFeedRepost.Record,
|
|
})
|
|
|
|
await network.bsky.sub.indexingSvc.indexRecord(...originalPost)
|
|
await network.bsky.sub.indexingSvc.indexRecord(...ownLike)
|
|
await network.bsky.sub.indexingSvc.indexRecord(...ownRepost)
|
|
await network.bsky.sub.indexingSvc.indexRecord(...aliceLike)
|
|
await network.bsky.sub.indexingSvc.indexRecord(...aliceRepost)
|
|
await network.bsky.sub.background.processAll()
|
|
|
|
const {
|
|
data: { notifications },
|
|
} = await agent.api.app.bsky.notification.listNotifications(
|
|
{},
|
|
{ headers: await network.serviceHeaders(sc.dids.bob) },
|
|
)
|
|
|
|
expect(notifications).toHaveLength(2)
|
|
expect(
|
|
notifications.every((n) => {
|
|
return n.author.did !== sc.dids.bob
|
|
}),
|
|
).toBeTruthy()
|
|
|
|
// Cleanup
|
|
const del = (uri: AtUri) => {
|
|
return prepareDelete({
|
|
did: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
}
|
|
|
|
await network.bsky.sub.indexingSvc.deleteRecord(...del(ownLike[0]))
|
|
await network.bsky.sub.indexingSvc.deleteRecord(...del(ownRepost[0]))
|
|
await network.bsky.sub.indexingSvc.deleteRecord(...del(aliceLike[0]))
|
|
await network.bsky.sub.indexingSvc.deleteRecord(...del(aliceRepost[0]))
|
|
await network.bsky.sub.indexingSvc.deleteRecord(...del(originalPost[0]))
|
|
})
|
|
|
|
it('handles profile aggregations out of order.', async () => {
|
|
const createdAt = new Date().toISOString()
|
|
const unknownDid = 'did:example:unknown'
|
|
const follow = await prepareCreate({
|
|
did: sc.dids.bob,
|
|
collection: ids.AppBskyGraphFollow,
|
|
record: {
|
|
$type: ids.AppBskyGraphFollow,
|
|
subject: unknownDid,
|
|
createdAt,
|
|
} as AppBskyGraphFollow.Record,
|
|
})
|
|
await network.bsky.sub.indexingSvc.indexRecord(...follow)
|
|
await network.bsky.sub.background.processAll()
|
|
const agg = await db.db
|
|
.selectFrom('profile_agg')
|
|
.select(['did', 'followersCount'])
|
|
.where('did', '=', unknownDid)
|
|
.executeTakeFirst()
|
|
expect(agg).toEqual({
|
|
did: unknownDid,
|
|
followersCount: 1,
|
|
})
|
|
// Cleanup
|
|
const del = (uri: AtUri) => {
|
|
return prepareDelete({
|
|
did: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
}
|
|
await network.bsky.sub.indexingSvc.deleteRecord(...del(follow[0]))
|
|
})
|
|
|
|
describe('indexRepo', () => {
|
|
beforeAll(async () => {
|
|
network.bsky.sub.run()
|
|
await basicSeed(sc, false)
|
|
await network.processAll()
|
|
await network.bsky.sub.destroy()
|
|
await network.bsky.sub.background.processAll()
|
|
})
|
|
|
|
it('preserves indexes when no record changes.', async () => {
|
|
// Mark originals
|
|
const { data: origProfile } = await agent.api.app.bsky.actor.getProfile(
|
|
{ actor: sc.dids.alice },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
const { data: origFeed } = await agent.api.app.bsky.feed.getAuthorFeed(
|
|
{ actor: sc.dids.alice },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
const { data: origFollows } = await agent.api.app.bsky.graph.getFollows(
|
|
{ actor: sc.dids.alice },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
// Index
|
|
const { data: commit } =
|
|
await pdsAgent.api.com.atproto.sync.getLatestCommit({
|
|
did: sc.dids.alice,
|
|
})
|
|
await network.bsky.sub.indexingSvc.indexRepo(sc.dids.alice, commit.cid)
|
|
await network.bsky.sub.background.processAll()
|
|
// Check
|
|
const { data: profile } = await agent.api.app.bsky.actor.getProfile(
|
|
{ actor: sc.dids.alice },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
const { data: feed } = await agent.api.app.bsky.feed.getAuthorFeed(
|
|
{ actor: sc.dids.alice },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
const { data: follows } = await agent.api.app.bsky.graph.getFollows(
|
|
{ actor: sc.dids.alice },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
expect(forSnapshot([origProfile, origFeed, origFollows])).toEqual(
|
|
forSnapshot([profile, feed, follows]),
|
|
)
|
|
})
|
|
|
|
it('updates indexes when records change.', async () => {
|
|
// Update profile
|
|
await pdsAgent.api.com.atproto.repo.putRecord(
|
|
{
|
|
repo: sc.dids.alice,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: 'self',
|
|
record: { description: 'freshening things up' },
|
|
},
|
|
{ headers: sc.getHeaders(sc.dids.alice), encoding: 'application/json' },
|
|
)
|
|
// Add post
|
|
const newPost = await sc.post(sc.dids.alice, 'fresh post!')
|
|
// Remove a follow
|
|
const removedFollow = sc.follows[sc.dids.alice][sc.dids.carol]
|
|
await pdsAgent.api.app.bsky.graph.follow.delete(
|
|
{ repo: sc.dids.alice, rkey: removedFollow.uri.rkey },
|
|
sc.getHeaders(sc.dids.alice),
|
|
)
|
|
// Index
|
|
const { data: commit } =
|
|
await pdsAgent.api.com.atproto.sync.getLatestCommit({
|
|
did: sc.dids.alice,
|
|
})
|
|
await network.bsky.sub.indexingSvc.indexRepo(sc.dids.alice, commit.cid)
|
|
await network.bsky.sub.background.processAll()
|
|
// Check
|
|
const { data: profile } = await agent.api.app.bsky.actor.getProfile(
|
|
{ actor: sc.dids.alice },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
const { data: feed } = await agent.api.app.bsky.feed.getAuthorFeed(
|
|
{ actor: sc.dids.alice },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
const { data: follows } = await agent.api.app.bsky.graph.getFollows(
|
|
{ actor: sc.dids.alice },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
expect(profile.description).toEqual('freshening things up')
|
|
expect(feed.feed[0].post.uri).toEqual(newPost.ref.uriStr)
|
|
expect(feed.feed[0].post.cid).toEqual(newPost.ref.cidStr)
|
|
expect(follows.follows.map(({ did }) => did)).not.toContain(sc.dids.carol)
|
|
expect(forSnapshot([profile, feed, follows])).toMatchSnapshot()
|
|
})
|
|
|
|
it('skips invalid records.', async () => {
|
|
const { accountManager } = network.pds.ctx
|
|
// const { db: pdsDb, services: pdsServices } = network.pds.ctx
|
|
// Create a good and a bad post record
|
|
const writes = await Promise.all([
|
|
repoPrepare.prepareCreate({
|
|
did: sc.dids.alice,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: { text: 'valid', createdAt: new Date().toISOString() },
|
|
}),
|
|
repoPrepare.prepareCreate({
|
|
did: sc.dids.alice,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: { text: 0 },
|
|
validate: false,
|
|
}),
|
|
])
|
|
const writeCommit = await network.pds.ctx.actorStore.transact(
|
|
sc.dids.alice,
|
|
(store) => store.repo.processWrites(writes),
|
|
)
|
|
await accountManager.updateRepoRoot(
|
|
sc.dids.alice,
|
|
writeCommit.cid,
|
|
writeCommit.rev,
|
|
)
|
|
await network.pds.ctx.sequencer.sequenceCommit(
|
|
sc.dids.alice,
|
|
writeCommit,
|
|
writes,
|
|
)
|
|
// Index
|
|
const { data: commit } =
|
|
await pdsAgent.api.com.atproto.sync.getLatestCommit({
|
|
did: sc.dids.alice,
|
|
})
|
|
await network.bsky.sub.indexingSvc.indexRepo(sc.dids.alice, commit.cid)
|
|
// Check
|
|
const getGoodPost = agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: writes[0].uri.toString(), depth: 0 },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
await expect(getGoodPost).resolves.toBeDefined()
|
|
const getBadPost = agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: writes[1].uri.toString(), depth: 0 },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
await expect(getBadPost).rejects.toThrow('Post not found')
|
|
})
|
|
})
|
|
|
|
describe('indexHandle', () => {
|
|
const getIndexedHandle = async (did) => {
|
|
const res = await agent.api.app.bsky.actor.getProfile(
|
|
{ actor: did },
|
|
{ headers: await network.serviceHeaders(sc.dids.alice) },
|
|
)
|
|
return res.data.handle
|
|
}
|
|
|
|
it('indexes handle for a fresh did', async () => {
|
|
const now = new Date().toISOString()
|
|
const sessionAgent = new AtpAgent({ service: network.pds.url })
|
|
const {
|
|
data: { did },
|
|
} = await sessionAgent.createAccount({
|
|
email: 'did1@test.com',
|
|
handle: 'did1.test',
|
|
password: 'password',
|
|
})
|
|
await expect(getIndexedHandle(did)).rejects.toThrow('Profile not found')
|
|
await network.bsky.sub.indexingSvc.indexHandle(did, now)
|
|
await expect(getIndexedHandle(did)).resolves.toEqual('did1.test')
|
|
})
|
|
|
|
it('reindexes handle for existing did when forced', async () => {
|
|
const now = new Date().toISOString()
|
|
const sessionAgent = new AtpAgent({ service: network.pds.url })
|
|
const {
|
|
data: { did },
|
|
} = await sessionAgent.createAccount({
|
|
email: 'did2@test.com',
|
|
handle: 'did2.test',
|
|
password: 'password',
|
|
})
|
|
await network.bsky.sub.indexingSvc.indexHandle(did, now)
|
|
await expect(getIndexedHandle(did)).resolves.toEqual('did2.test')
|
|
await sessionAgent.com.atproto.identity.updateHandle({
|
|
handle: 'did2-updated.test',
|
|
})
|
|
await network.bsky.sub.indexingSvc.indexHandle(did, now)
|
|
await expect(getIndexedHandle(did)).resolves.toEqual('did2.test') // Didn't update, not forced
|
|
await network.bsky.sub.indexingSvc.indexHandle(did, now, true)
|
|
await expect(getIndexedHandle(did)).resolves.toEqual('did2-updated.test')
|
|
})
|
|
|
|
it('handles profile aggregations out of order', async () => {
|
|
const now = new Date().toISOString()
|
|
const sessionAgent = new AtpAgent({ service: network.pds.url })
|
|
const {
|
|
data: { did },
|
|
} = await sessionAgent.createAccount({
|
|
email: 'did3@test.com',
|
|
handle: 'did3.test',
|
|
password: 'password',
|
|
})
|
|
const follow = await prepareCreate({
|
|
did: sc.dids.bob,
|
|
collection: ids.AppBskyGraphFollow,
|
|
record: {
|
|
$type: ids.AppBskyGraphFollow,
|
|
subject: did,
|
|
createdAt: now,
|
|
} as AppBskyGraphFollow.Record,
|
|
})
|
|
await network.bsky.sub.indexingSvc.indexRecord(...follow)
|
|
await network.bsky.sub.indexingSvc.indexHandle(did, now)
|
|
await network.bsky.sub.background.processAll()
|
|
const agg = await db.db
|
|
.selectFrom('profile_agg')
|
|
.select(['did', 'followersCount'])
|
|
.where('did', '=', did)
|
|
.executeTakeFirst()
|
|
expect(agg).toEqual({
|
|
did,
|
|
followersCount: 1,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('tombstoneActor', () => {
|
|
it('does not unindex actor when they are still being hosted by their pds', async () => {
|
|
const { data: profileBefore } = await agent.api.app.bsky.actor.getProfile(
|
|
{ actor: sc.dids.alice },
|
|
{ headers: await network.serviceHeaders(sc.dids.bob) },
|
|
)
|
|
// Attempt indexing tombstone
|
|
await network.bsky.sub.indexingSvc.tombstoneActor(sc.dids.alice)
|
|
const { data: profileAfter } = await agent.api.app.bsky.actor.getProfile(
|
|
{ actor: sc.dids.alice },
|
|
{ headers: await network.serviceHeaders(sc.dids.bob) },
|
|
)
|
|
expect(profileAfter).toEqual(profileBefore)
|
|
})
|
|
|
|
it('unindexes actor when they are no longer hosted by their pds', async () => {
|
|
const { alice } = sc.dids
|
|
const getProfileBefore = agent.api.app.bsky.actor.getProfile(
|
|
{ actor: alice },
|
|
{ headers: await network.serviceHeaders(sc.dids.bob) },
|
|
)
|
|
await expect(getProfileBefore).resolves.toBeDefined()
|
|
// Delete account on pds
|
|
const token = await network.pds.ctx.accountManager.createEmailToken(
|
|
alice,
|
|
'delete_account',
|
|
)
|
|
await pdsAgent.api.com.atproto.server.deleteAccount({
|
|
token,
|
|
did: alice,
|
|
password: sc.accounts[alice].password,
|
|
})
|
|
await network.pds.ctx.backgroundQueue.processAll()
|
|
// Index tombstone
|
|
await network.bsky.sub.indexingSvc.tombstoneActor(alice)
|
|
const getProfileAfter = agent.api.app.bsky.actor.getProfile(
|
|
{ actor: alice },
|
|
{ headers: await network.serviceHeaders(sc.dids.bob) },
|
|
)
|
|
await expect(getProfileAfter).rejects.toThrow('Profile not found')
|
|
})
|
|
})
|
|
|
|
async function getNotifications(db: Database, uri: AtUri) {
|
|
return await db.db
|
|
.selectFrom('notification')
|
|
.selectAll()
|
|
.select(sql`0`.as('id')) // Ignore notification ids in comparisons
|
|
.where('recordUri', '=', uri.toString())
|
|
.orderBy('sortAt')
|
|
.execute()
|
|
}
|
|
})
|
|
|
|
async function prepareCreate(opts: {
|
|
did: string
|
|
collection: string
|
|
rkey?: string
|
|
record: unknown
|
|
timestamp?: string
|
|
}): Promise<[AtUri, CID, unknown, WriteOpAction.Create, string]> {
|
|
const rkey = opts.rkey ?? TID.nextStr()
|
|
return [
|
|
AtUri.make(opts.did, opts.collection, rkey),
|
|
await cidForCbor(opts.record),
|
|
opts.record,
|
|
WriteOpAction.Create,
|
|
opts.timestamp ?? new Date().toISOString(),
|
|
]
|
|
}
|
|
|
|
async function prepareUpdate(opts: {
|
|
did: string
|
|
collection: string
|
|
rkey: string
|
|
record: unknown
|
|
timestamp?: string
|
|
}): Promise<[AtUri, CID, unknown, WriteOpAction.Update, string]> {
|
|
return [
|
|
AtUri.make(opts.did, opts.collection, opts.rkey),
|
|
await cidForCbor(opts.record),
|
|
opts.record,
|
|
WriteOpAction.Update,
|
|
opts.timestamp ?? new Date().toISOString(),
|
|
]
|
|
}
|
|
|
|
function prepareDelete(opts: {
|
|
did: string
|
|
collection: string
|
|
rkey: string
|
|
}): [AtUri] {
|
|
return [AtUri.make(opts.did, opts.collection, opts.rkey)]
|
|
}
|