638f5a8312
* Fix avatar path resolution in dev-env * changeset * extract dev-env assets to dedicated folder * add comment * fix fmt
1587 lines
48 KiB
TypeScript
1587 lines
48 KiB
TypeScript
import fs from 'fs/promises'
|
|
import { AtUri } from '@atproto/syntax'
|
|
import { AtpAgent } from '@atproto/api'
|
|
import { BlobRef } from '@atproto/lexicon'
|
|
import { TestNetworkNoAppView } from '@atproto/dev-env'
|
|
import { cidForCbor, TID, ui8ToArrayBuffer } from '@atproto/common'
|
|
import { BlobNotFoundError } from '@atproto/repo'
|
|
import * as Post from '../src/lexicon/types/app/bsky/feed/post'
|
|
import { forSnapshot, paginateAll } from './_util'
|
|
import AppContext from '../src/context'
|
|
import { ids, lexicons } from '../src/lexicon/lexicons'
|
|
|
|
describe('crud operations', () => {
|
|
let network: TestNetworkNoAppView
|
|
let ctx: AppContext
|
|
let agent: AtpAgent
|
|
let aliceAgent: AtpAgent
|
|
let bobAgent: AtpAgent
|
|
|
|
beforeAll(async () => {
|
|
network = await TestNetworkNoAppView.create({
|
|
dbPostgresSchema: 'crud',
|
|
})
|
|
// @ts-expect-error Error due to circular dependency with the dev-env package
|
|
ctx = network.pds.ctx
|
|
agent = network.pds.getClient()
|
|
aliceAgent = network.pds.getClient()
|
|
bobAgent = network.pds.getClient()
|
|
|
|
await aliceAgent.createAccount({
|
|
email: 'alice@test.com',
|
|
handle: 'alice.test',
|
|
password: 'alice-pass',
|
|
})
|
|
|
|
await bobAgent.createAccount({
|
|
email: 'bob@test.com',
|
|
handle: 'bob.test',
|
|
password: 'bob-pass',
|
|
})
|
|
|
|
expect(bobAgent.accountDid).not.toBe(aliceAgent.accountDid)
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await network.close()
|
|
})
|
|
|
|
it('registers users', async () => {
|
|
const res = await agent.api.com.atproto.server.createAccount({
|
|
handle: 'user1.test',
|
|
email: 'user1@test.com',
|
|
password: 'password',
|
|
})
|
|
expect(res.data.handle).toBe('user1.test')
|
|
expect(res.data.accessJwt).toBeDefined()
|
|
})
|
|
|
|
it('describes repo', async () => {
|
|
const description = await agent.api.com.atproto.repo.describeRepo({
|
|
repo: aliceAgent.accountDid,
|
|
})
|
|
expect(description.data.handle).toBe('alice.test')
|
|
expect(description.data.did).toBe(aliceAgent.accountDid)
|
|
const description2 = await agent.api.com.atproto.repo.describeRepo({
|
|
repo: bobAgent.accountDid,
|
|
})
|
|
expect(description2.data.handle).toBe('bob.test')
|
|
expect(description2.data.did).toBe(bobAgent.accountDid)
|
|
})
|
|
|
|
it('creates records', async () => {
|
|
const res = await aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.post',
|
|
record: {
|
|
$type: 'app.bsky.feed.post',
|
|
text: 'Hello, world!',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
})
|
|
const uri = new AtUri(res.data.uri)
|
|
expect(res.data.uri).toBe(
|
|
`at://${aliceAgent.accountDid}/app.bsky.feed.post/${uri.rkey}`,
|
|
)
|
|
|
|
const res1 = await agent.api.com.atproto.repo.listRecords({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.post',
|
|
})
|
|
expect(res1.data.records.length).toBe(1)
|
|
expect(res1.data.records[0].uri).toBe(uri.toString())
|
|
expect((res1.data.records[0].value as Post.Record).text).toBe(
|
|
'Hello, world!',
|
|
)
|
|
|
|
const res2 = await agent.app.bsky.feed.post.list({
|
|
repo: aliceAgent.accountDid,
|
|
})
|
|
expect(res2.records.length).toBe(1)
|
|
expect(res2.records[0].uri).toBe(uri.toString())
|
|
expect(res2.records[0].value.text).toBe('Hello, world!')
|
|
|
|
const res3 = await agent.api.com.atproto.repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.post',
|
|
rkey: uri.rkey,
|
|
})
|
|
expect(res3.data.uri).toBe(uri.toString())
|
|
expect((res3.data.value as Post.Record).text).toBe('Hello, world!')
|
|
|
|
const res4 = await agent.app.bsky.feed.post.get({
|
|
repo: aliceAgent.accountDid,
|
|
rkey: uri.rkey,
|
|
})
|
|
expect(res4.uri).toBe(uri.toString())
|
|
expect(res4.value.text).toBe('Hello, world!')
|
|
|
|
await aliceAgent.api.com.atproto.repo.deleteRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.post',
|
|
rkey: uri.rkey,
|
|
})
|
|
const res5 = await agent.api.com.atproto.repo.listRecords({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.post',
|
|
})
|
|
expect(res5.data.records.length).toBe(0)
|
|
})
|
|
|
|
it('CRUDs records with the semantic sugars', async () => {
|
|
const res1 = await aliceAgent.app.bsky.feed.post.create(
|
|
{ repo: aliceAgent.accountDid },
|
|
{
|
|
$type: 'app.bsky.feed.post',
|
|
text: 'Hello, world!',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
)
|
|
const uri = new AtUri(res1.uri)
|
|
|
|
const res2 = await agent.app.bsky.feed.post.list({
|
|
repo: aliceAgent.accountDid,
|
|
})
|
|
expect(res2.records.length).toBe(1)
|
|
|
|
await aliceAgent.app.bsky.feed.post.delete({
|
|
repo: aliceAgent.accountDid,
|
|
rkey: uri.rkey,
|
|
})
|
|
|
|
const res3 = await agent.app.bsky.feed.post.list({
|
|
repo: aliceAgent.accountDid,
|
|
})
|
|
expect(res3.records.length).toBe(0)
|
|
})
|
|
|
|
it('attaches images to a post', async () => {
|
|
const file = await fs.readFile('../dev-env/assets/key-landscape-small.jpg')
|
|
const uploadedRes = await aliceAgent.api.com.atproto.repo.uploadBlob(file, {
|
|
encoding: 'image/jpeg',
|
|
})
|
|
const uploaded = uploadedRes.data.blob
|
|
// Expect blobstore not to have image yet
|
|
await expect(
|
|
ctx.blobstore(aliceAgent.accountDid).getBytes(uploaded.ref),
|
|
).rejects.toThrow(BlobNotFoundError)
|
|
// Associate image with post, image should be placed in blobstore
|
|
const res = await aliceAgent.app.bsky.feed.post.create(
|
|
{ repo: aliceAgent.accountDid },
|
|
{
|
|
$type: 'app.bsky.feed.post',
|
|
text: "Here's a key!",
|
|
createdAt: new Date().toISOString(),
|
|
embed: {
|
|
$type: 'app.bsky.embed.images',
|
|
images: [{ image: uploaded, alt: '' }],
|
|
},
|
|
},
|
|
)
|
|
// Ensure image is on post record
|
|
const postUri = new AtUri(res.uri)
|
|
const post = await aliceAgent.app.bsky.feed.post.get({
|
|
rkey: postUri.rkey,
|
|
repo: aliceAgent.accountDid,
|
|
})
|
|
const images = post.value.embed?.images as { image: BlobRef }[]
|
|
expect(images.length).toEqual(1)
|
|
expect(uploaded.ref.equals(images[0].image.ref)).toBeTruthy()
|
|
// Ensure that the uploaded image is now in the blobstore, i.e. doesn't throw BlobNotFoundError
|
|
await ctx.blobstore(aliceAgent.accountDid).getBytes(uploaded.ref)
|
|
// Cleanup
|
|
await aliceAgent.app.bsky.feed.post.delete({
|
|
rkey: postUri.rkey,
|
|
repo: aliceAgent.accountDid,
|
|
})
|
|
})
|
|
|
|
it('creates records with the correct key described by the schema', async () => {
|
|
const res1 = await aliceAgent.app.bsky.actor.profile.create(
|
|
{ repo: aliceAgent.accountDid },
|
|
{
|
|
displayName: 'alice',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
)
|
|
const uri = new AtUri(res1.uri)
|
|
expect(uri.rkey).toBe('self')
|
|
})
|
|
|
|
describe('paginates', () => {
|
|
let uri1: AtUri
|
|
let uri2: AtUri
|
|
let uri3: AtUri
|
|
let uri4: AtUri
|
|
let uri5: AtUri
|
|
|
|
beforeAll(async () => {
|
|
const createPost = async (text: string) => {
|
|
const res = await aliceAgent.app.bsky.feed.post.create(
|
|
{ repo: aliceAgent.accountDid },
|
|
{
|
|
$type: 'app.bsky.feed.post',
|
|
text,
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
)
|
|
return new AtUri(res.uri)
|
|
}
|
|
uri1 = await createPost('Post 1')
|
|
uri2 = await createPost('Post 2')
|
|
uri3 = await createPost('Post 3')
|
|
uri4 = await createPost('Post 4')
|
|
uri5 = await createPost('Post 5')
|
|
})
|
|
|
|
afterAll(async () => {
|
|
for (const uri of [uri1, uri2, uri3, uri4, uri5]) {
|
|
await aliceAgent.app.bsky.feed.post.delete({
|
|
repo: aliceAgent.accountDid,
|
|
rkey: uri.rkey,
|
|
})
|
|
}
|
|
})
|
|
|
|
it('in forwards order', async () => {
|
|
const results = (results) => results.flatMap((res) => res.records)
|
|
const paginator = async (cursor?: string) => {
|
|
const res = await agent.app.bsky.feed.post.list({
|
|
repo: aliceAgent.accountDid,
|
|
cursor,
|
|
limit: 2,
|
|
})
|
|
return res
|
|
}
|
|
|
|
const paginatedAll = await paginateAll(paginator)
|
|
paginatedAll.forEach((res) =>
|
|
expect(res.records.length).toBeLessThanOrEqual(2),
|
|
)
|
|
|
|
const full = await agent.app.bsky.feed.post.list({
|
|
repo: aliceAgent.accountDid,
|
|
})
|
|
|
|
expect(full.records.length).toEqual(5)
|
|
expect(results(paginatedAll)).toEqual(results([full]))
|
|
})
|
|
|
|
it('in reverse order', async () => {
|
|
const results = (results) => results.flatMap((res) => res.records)
|
|
const paginator = async (cursor?: string) => {
|
|
const res = await agent.app.bsky.feed.post.list({
|
|
repo: aliceAgent.accountDid,
|
|
reverse: true,
|
|
cursor,
|
|
limit: 2,
|
|
})
|
|
return res
|
|
}
|
|
|
|
const paginatedAll = await paginateAll(paginator)
|
|
paginatedAll.forEach((res) =>
|
|
expect(res.records.length).toBeLessThanOrEqual(2),
|
|
)
|
|
|
|
const full = await agent.app.bsky.feed.post.list({
|
|
repo: aliceAgent.accountDid,
|
|
reverse: true,
|
|
})
|
|
|
|
expect(full.records.length).toEqual(5)
|
|
expect(results(paginatedAll)).toEqual(results([full]))
|
|
})
|
|
|
|
it('reverses', async () => {
|
|
const forwards = await agent.app.bsky.feed.post.list({
|
|
repo: aliceAgent.accountDid,
|
|
})
|
|
const reverse = await agent.app.bsky.feed.post.list({
|
|
repo: aliceAgent.accountDid,
|
|
reverse: true,
|
|
})
|
|
expect(forwards.cursor).toEqual(uri1.rkey)
|
|
expect(reverse.cursor).toEqual(uri5.rkey)
|
|
expect(forwards.records.length).toEqual(5)
|
|
expect(reverse.records.length).toEqual(5)
|
|
expect(forwards.records.reverse()).toEqual(reverse.records)
|
|
})
|
|
})
|
|
|
|
describe('deleteRecord', () => {
|
|
it('deletes a record if it exists', async () => {
|
|
const { repo } = aliceAgent.api.com.atproto
|
|
const { data: post } = await repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: { text: 'post', createdAt: new Date().toISOString() },
|
|
})
|
|
const uri = new AtUri(post.uri)
|
|
await repo.deleteRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
const checkPost = repo.getRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
await expect(checkPost).rejects.toThrow('Could not locate record')
|
|
})
|
|
|
|
it("no-ops if record doesn't exist", async () => {
|
|
const { repo } = aliceAgent.api.com.atproto
|
|
const { data: post } = await repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: { text: 'post', createdAt: new Date().toISOString() },
|
|
})
|
|
const uri = new AtUri(post.uri)
|
|
await repo.deleteRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
const checkPost = repo.getRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
await expect(checkPost).rejects.toThrow('Could not locate record')
|
|
const attemptDelete = repo.deleteRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
await expect(attemptDelete).resolves.toBeDefined()
|
|
})
|
|
|
|
it('does not delete the underlying block if it is referenced elsewhere', async () => {
|
|
const { repo } = aliceAgent.api.com.atproto
|
|
const record = { text: 'post', createdAt: new Date().toISOString() }
|
|
const { data: post1 } = await repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyFeedPost,
|
|
record,
|
|
})
|
|
const { data: post2 } = await repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyFeedPost,
|
|
record,
|
|
})
|
|
const uri1 = new AtUri(post1.uri)
|
|
await repo.deleteRecord({
|
|
repo: uri1.host,
|
|
collection: uri1.collection,
|
|
rkey: uri1.rkey,
|
|
})
|
|
const uri2 = new AtUri(post2.uri)
|
|
const checkPost2 = await repo.getRecord({
|
|
repo: uri2.host,
|
|
collection: uri2.collection,
|
|
rkey: uri2.rkey,
|
|
})
|
|
expect(checkPost2).toBeDefined()
|
|
expect(checkPost2.data.value).toMatchObject(record)
|
|
})
|
|
})
|
|
|
|
describe('putRecord', () => {
|
|
const profilePath = {
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: 'self',
|
|
}
|
|
|
|
it("creates a new record if it doesn't already exist", async () => {
|
|
const { repo } = bobAgent.api.com.atproto
|
|
const exists = repo.getRecord({
|
|
...profilePath,
|
|
repo: bobAgent.accountDid,
|
|
})
|
|
await expect(exists).rejects.toThrow('Could not locate record')
|
|
|
|
const { data: put } = await repo.putRecord({
|
|
...profilePath,
|
|
repo: bobAgent.accountDid,
|
|
record: {
|
|
displayName: 'Robert',
|
|
},
|
|
})
|
|
expect(put.uri).toEqual(
|
|
`at://${bobAgent.accountDid}/${ids.AppBskyActorProfile}/self`,
|
|
)
|
|
|
|
const { data: profile } = await repo.getRecord({
|
|
...profilePath,
|
|
repo: bobAgent.accountDid,
|
|
})
|
|
expect(profile.value).toEqual({
|
|
$type: ids.AppBskyActorProfile,
|
|
displayName: 'Robert',
|
|
})
|
|
})
|
|
|
|
it('updates a record if it already exists', async () => {
|
|
const { repo } = bobAgent.api.com.atproto
|
|
const { data: put } = await repo.putRecord({
|
|
...profilePath,
|
|
repo: bobAgent.accountDid,
|
|
record: {
|
|
displayName: 'Robert',
|
|
description: 'Dog lover',
|
|
},
|
|
})
|
|
expect(put.uri).toEqual(
|
|
`at://${bobAgent.accountDid}/${ids.AppBskyActorProfile}/self`,
|
|
)
|
|
|
|
const { data: profile } = await repo.getRecord({
|
|
...profilePath,
|
|
repo: bobAgent.accountDid,
|
|
})
|
|
expect(profile.value).toEqual({
|
|
$type: ids.AppBskyActorProfile,
|
|
displayName: 'Robert',
|
|
description: 'Dog lover',
|
|
})
|
|
})
|
|
|
|
it('does not produce commit on no-op update', async () => {
|
|
const { repo } = bobAgent.api.com.atproto
|
|
const rootRes1 = await bobAgent.api.com.atproto.sync.getLatestCommit({
|
|
did: bobAgent.accountDid,
|
|
})
|
|
const { data: put } = await repo.putRecord({
|
|
...profilePath,
|
|
repo: bobAgent.accountDid,
|
|
record: {
|
|
displayName: 'Robert',
|
|
description: 'Dog lover',
|
|
},
|
|
})
|
|
expect(put.uri).toEqual(
|
|
`at://${bobAgent.accountDid}/${ids.AppBskyActorProfile}/self`,
|
|
)
|
|
|
|
const rootRes2 = await bobAgent.api.com.atproto.sync.getLatestCommit({
|
|
did: bobAgent.accountDid,
|
|
})
|
|
|
|
expect(rootRes2.data.cid).toEqual(rootRes1.data.cid)
|
|
expect(rootRes2.data.rev).toEqual(rootRes1.data.rev)
|
|
})
|
|
|
|
it('fails on user mismatch', async () => {
|
|
const { repo } = aliceAgent.api.com.atproto
|
|
const put = repo.putRecord({
|
|
repo: bobAgent.accountDid,
|
|
collection: ids.AppBskyGraphFollow,
|
|
rkey: TID.nextStr(),
|
|
record: {
|
|
subject: aliceAgent.accountDid,
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
})
|
|
await expect(put).rejects.toThrow('Authentication Required')
|
|
})
|
|
|
|
it('fails on invalid record', async () => {
|
|
const { repo } = bobAgent.api.com.atproto
|
|
const put = repo.putRecord({
|
|
...profilePath,
|
|
repo: bobAgent.accountDid,
|
|
record: {
|
|
displayName: 'Robert',
|
|
description: 3.141,
|
|
},
|
|
})
|
|
await expect(put).rejects.toThrow(
|
|
'Invalid app.bsky.actor.profile record: Record/description must be a string',
|
|
)
|
|
const { data: profile } = await repo.getRecord({
|
|
...profilePath,
|
|
repo: bobAgent.accountDid,
|
|
})
|
|
expect(profile.value).toEqual({
|
|
$type: ids.AppBskyActorProfile,
|
|
displayName: 'Robert',
|
|
description: 'Dog lover',
|
|
})
|
|
})
|
|
|
|
// @TODO remove after migrating legacy blobs
|
|
it('updates a legacy blob ref when updating profile', async () => {
|
|
const { repo } = bobAgent.api.com.atproto
|
|
const file = await fs.readFile('../dev-env/assets/key-portrait-small.jpg')
|
|
const uploadedRes = await repo.uploadBlob(file, {
|
|
encoding: 'image/jpeg',
|
|
})
|
|
|
|
await repo.putRecord({
|
|
...profilePath,
|
|
repo: bobAgent.accountDid,
|
|
record: {
|
|
displayName: 'Robert',
|
|
avatar: BlobRef.fromJsonRef({
|
|
mimeType: uploadedRes.data.blob.mimeType,
|
|
cid: uploadedRes.data.blob.ref.toString(),
|
|
}),
|
|
},
|
|
})
|
|
|
|
const got = await repo.getRecord({
|
|
...profilePath,
|
|
repo: bobAgent.accountDid,
|
|
})
|
|
const gotAvatar = got.data.value['avatar'] as BlobRef
|
|
expect(gotAvatar.original).toEqual(uploadedRes.data.blob.original)
|
|
})
|
|
})
|
|
|
|
// Validation
|
|
// --------------
|
|
|
|
it('defaults an undefined $type on records', async () => {
|
|
const res = await aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.post',
|
|
record: {
|
|
text: 'blah',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
})
|
|
const uri = new AtUri(res.data.uri)
|
|
const got = await agent.api.com.atproto.repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
expect(got.data.value['$type']).toBe(uri.collection)
|
|
})
|
|
|
|
it('requires the schema to be known if explicitly validating', async () => {
|
|
const prom = aliceAgent.api.com.atproto.repo.createRecord({
|
|
validate: true,
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'com.example.foobar',
|
|
record: { $type: 'com.example.foobar' },
|
|
})
|
|
await expect(prom).rejects.toThrow(
|
|
'Lexicon not found: lex:com.example.foobar',
|
|
)
|
|
})
|
|
|
|
it('does not require the schema to be known if not explicitly validating', async () => {
|
|
const prom = await aliceAgent.api.com.atproto.repo.createRecord({
|
|
// validate not set
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'com.example.foobar',
|
|
record: { $type: 'com.example.foobar' },
|
|
})
|
|
expect(prom.data.validationStatus).toBe('unknown')
|
|
})
|
|
|
|
it('requires the $type to match the schema', async () => {
|
|
await expect(
|
|
aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.post',
|
|
record: { $type: 'app.bsky.feed.like' },
|
|
}),
|
|
).rejects.toThrow(
|
|
'Invalid $type: expected app.bsky.feed.post, got app.bsky.feed.like',
|
|
)
|
|
})
|
|
|
|
it('requires valid rkey', async () => {
|
|
await expect(
|
|
aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.generator',
|
|
record: {
|
|
$type: 'app.bsky.feed.generator',
|
|
did: 'did:web:dummy.example.com',
|
|
displayName: 'dummy',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
rkey: '..',
|
|
}),
|
|
).rejects.toThrow('record key can not be "." or ".."')
|
|
})
|
|
|
|
it('validates the record on write', async () => {
|
|
await expect(
|
|
aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.post',
|
|
record: { $type: 'app.bsky.feed.post' },
|
|
}),
|
|
).rejects.toThrow(
|
|
'Invalid app.bsky.feed.post record: Record must have the property "text"',
|
|
)
|
|
})
|
|
|
|
it('validates datetimes more rigorously than lex sdk', async () => {
|
|
const postRecord = {
|
|
$type: 'app.bsky.feed.post',
|
|
text: 'test',
|
|
createdAt: '1985-04-12T23:20:50.123',
|
|
}
|
|
lexicons.assertValidRecord('app.bsky.feed.post', postRecord)
|
|
await expect(
|
|
aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.post',
|
|
record: postRecord,
|
|
}),
|
|
).rejects.toThrow(
|
|
'Invalid app.bsky.feed.post record: createdAt must be an valid atproto datetime (both RFC-3339 and ISO-8601)',
|
|
)
|
|
})
|
|
|
|
describe('unvalidated writes', () => {
|
|
it('disallows creation of unknown lexicons when validate is set to true', async () => {
|
|
const attempt = aliceAgent.api.com.atproto.repo.createRecord({
|
|
validate: true,
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'com.example.record',
|
|
record: {
|
|
blah: 'thing',
|
|
},
|
|
})
|
|
await expect(attempt).rejects.toThrow(
|
|
'Lexicon not found: lex:com.example.record',
|
|
)
|
|
})
|
|
|
|
it('allows creation of unknown lexicons when validate is not set to true', async () => {
|
|
// validate: default
|
|
const res1 = await aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'com.example.record',
|
|
record: {
|
|
blah: 'thing1',
|
|
},
|
|
})
|
|
expect(res1.data.validationStatus).toBe('unknown')
|
|
const record1 = await ctx.actorStore.read(
|
|
aliceAgent.accountDid,
|
|
(store) =>
|
|
store.record.getRecord(new AtUri(res1.data.uri), res1.data.cid),
|
|
)
|
|
expect(record1?.value).toEqual({
|
|
$type: 'com.example.record',
|
|
blah: 'thing1',
|
|
})
|
|
// validate: false
|
|
const res2 = await aliceAgent.api.com.atproto.repo.createRecord({
|
|
validate: false,
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'com.example.record',
|
|
record: {
|
|
blah: 'thing2',
|
|
},
|
|
})
|
|
expect(res2.data.validationStatus).toBeUndefined()
|
|
const record2 = await ctx.actorStore.read(
|
|
aliceAgent.accountDid,
|
|
(store) =>
|
|
store.record.getRecord(new AtUri(res2.data.uri), res2.data.cid),
|
|
)
|
|
expect(record2?.value).toEqual({
|
|
$type: 'com.example.record',
|
|
blah: 'thing2',
|
|
})
|
|
})
|
|
|
|
it('allows update of unknown lexicons when validate is set to false', async () => {
|
|
const createRes = await aliceAgent.api.com.atproto.repo.createRecord({
|
|
validate: false,
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'com.example.record',
|
|
record: {
|
|
blah: 'thing',
|
|
},
|
|
})
|
|
const uri = new AtUri(createRes.data.uri)
|
|
// validate: default
|
|
const updateRes1 = await aliceAgent.api.com.atproto.repo.putRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'com.example.record',
|
|
rkey: uri.rkey,
|
|
record: {
|
|
blah: 'something else',
|
|
},
|
|
})
|
|
const record1 = await ctx.actorStore.read(
|
|
aliceAgent.accountDid,
|
|
(store) => store.record.getRecord(uri, updateRes1.data.cid),
|
|
)
|
|
expect(record1?.value).toEqual({
|
|
$type: 'com.example.record',
|
|
blah: 'something else',
|
|
})
|
|
// validate: false
|
|
const updateRes2 = await aliceAgent.api.com.atproto.repo.putRecord({
|
|
validate: false,
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'com.example.record',
|
|
rkey: uri.rkey,
|
|
record: {
|
|
blah: 'something else',
|
|
},
|
|
})
|
|
const record2 = await ctx.actorStore.read(
|
|
aliceAgent.accountDid,
|
|
(store) => store.record.getRecord(uri, updateRes2.data.cid),
|
|
)
|
|
expect(record2?.value).toEqual({
|
|
$type: 'com.example.record',
|
|
blah: 'something else',
|
|
})
|
|
})
|
|
|
|
it('applyWrites returns results with validation status', async () => {
|
|
const existing1 = await aliceAgent.api.com.atproto.repo.createRecord({
|
|
validate: false,
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'com.example.record',
|
|
record: {
|
|
blah: 'thing1',
|
|
},
|
|
})
|
|
const existing2 = await aliceAgent.api.com.atproto.repo.createRecord({
|
|
validate: false,
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'com.example.record',
|
|
record: {
|
|
blah: 'thing2',
|
|
},
|
|
})
|
|
const {
|
|
data: { results },
|
|
} = await aliceAgent.com.atproto.repo.applyWrites({
|
|
repo: aliceAgent.accountDid,
|
|
writes: [
|
|
{
|
|
$type: `${ids.ComAtprotoRepoApplyWrites}#create`,
|
|
action: 'create',
|
|
collection: ids.AppBskyFeedPost,
|
|
value: {
|
|
$type: ids.AppBskyFeedPost,
|
|
text: '👋',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
},
|
|
{
|
|
$type: `${ids.ComAtprotoRepoApplyWrites}#update`,
|
|
action: 'update',
|
|
collection: 'com.example.record',
|
|
rkey: new AtUri(existing1.data.uri).rkey,
|
|
value: {},
|
|
},
|
|
{
|
|
$type: `${ids.ComAtprotoRepoApplyWrites}#delete`,
|
|
action: 'delete',
|
|
collection: 'com.example.record',
|
|
rkey: new AtUri(existing2.data.uri).rkey,
|
|
},
|
|
],
|
|
})
|
|
expect(forSnapshot(results)).toEqual([
|
|
{
|
|
$type: `${ids.ComAtprotoRepoApplyWrites}#createResult`,
|
|
cid: 'cids(0)',
|
|
uri: 'record(0)',
|
|
validationStatus: 'valid',
|
|
},
|
|
{
|
|
$type: `${ids.ComAtprotoRepoApplyWrites}#updateResult`,
|
|
cid: 'cids(1)',
|
|
uri: 'record(1)',
|
|
validationStatus: 'unknown',
|
|
},
|
|
{ $type: `${ids.ComAtprotoRepoApplyWrites}#deleteResult` },
|
|
])
|
|
})
|
|
|
|
it('correctly associates images with unknown record types', async () => {
|
|
const file = await fs.readFile('../dev-env/assets/key-portrait-small.jpg')
|
|
const uploadedRes = await aliceAgent.api.com.atproto.repo.uploadBlob(
|
|
file,
|
|
{
|
|
encoding: 'image/jpeg',
|
|
},
|
|
)
|
|
|
|
const res = await aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'com.example.record',
|
|
record: {
|
|
blah: 'thing',
|
|
image: uploadedRes.data.blob,
|
|
},
|
|
validate: false,
|
|
})
|
|
const record = await ctx.actorStore.read(aliceAgent.accountDid, (store) =>
|
|
store.record.getRecord(new AtUri(res.data.uri), res.data.cid),
|
|
)
|
|
expect(record?.value).toMatchObject({
|
|
$type: 'com.example.record',
|
|
blah: 'thing',
|
|
})
|
|
const recordBlobs = await ctx.actorStore.read(
|
|
aliceAgent.accountDid,
|
|
(store) =>
|
|
store.db.db
|
|
.selectFrom('blob')
|
|
.innerJoin('record_blob', 'record_blob.blobCid', 'blob.cid')
|
|
.where('recordUri', '=', res.data.uri)
|
|
.selectAll()
|
|
.execute(),
|
|
)
|
|
expect(recordBlobs.length).toBe(1)
|
|
expect(recordBlobs.at(0)?.cid).toBe(uploadedRes.data.blob.ref.toString())
|
|
})
|
|
|
|
it('enforces record type constraint even when unvalidated', async () => {
|
|
const attempt = aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'com.example.record',
|
|
record: {
|
|
$type: 'com.example.other',
|
|
blah: 'thing',
|
|
},
|
|
})
|
|
await expect(attempt).rejects.toThrow(
|
|
'Invalid $type: expected com.example.record, got com.example.other',
|
|
)
|
|
})
|
|
|
|
it('enforces blob ref format even when unvalidated', async () => {
|
|
const file = await fs.readFile('../dev-env/assets/key-portrait-small.jpg')
|
|
const uploadedRes = await aliceAgent.api.com.atproto.repo.uploadBlob(
|
|
file,
|
|
{
|
|
encoding: 'image/jpeg',
|
|
},
|
|
)
|
|
|
|
const attempt = aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'com.example.record',
|
|
record: {
|
|
blah: 'thing',
|
|
image: {
|
|
cid: uploadedRes.data.blob.ref.toString(),
|
|
mimeType: uploadedRes.data.blob.mimeType,
|
|
},
|
|
},
|
|
validate: false,
|
|
})
|
|
await expect(attempt).rejects.toThrow(`Legacy blob ref at 'image'`)
|
|
})
|
|
})
|
|
|
|
describe('compare-and-swap', () => {
|
|
let recordCount = 0 // Ensures unique cids
|
|
const postRecord = () => ({
|
|
text: `post (${++recordCount})`,
|
|
createdAt: new Date().toISOString(),
|
|
})
|
|
const profileRecord = () => ({
|
|
displayName: `ali (${++recordCount})`,
|
|
})
|
|
|
|
it('createRecord succeeds on proper commit cas', async () => {
|
|
const { repo, sync } = aliceAgent.api.com.atproto
|
|
const { data: commit } = await sync.getLatestCommit({
|
|
did: aliceAgent.accountDid,
|
|
})
|
|
const { data: post } = await repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyFeedPost,
|
|
swapCommit: commit.cid,
|
|
record: postRecord(),
|
|
})
|
|
const uri = new AtUri(post.uri)
|
|
const checkPost = repo.getRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
await expect(checkPost).resolves.toBeDefined()
|
|
})
|
|
|
|
it('createRecord fails on bad commit cas', async () => {
|
|
const { repo, sync } = aliceAgent.api.com.atproto
|
|
const { data: staleCommit } = await sync.getLatestCommit({
|
|
did: aliceAgent.accountDid,
|
|
})
|
|
// Update repo, change head
|
|
await repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: postRecord(),
|
|
})
|
|
const attemptCreate = repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyFeedPost,
|
|
swapCommit: staleCommit.cid,
|
|
record: postRecord(),
|
|
})
|
|
await expect(attemptCreate).rejects.toMatchObject({
|
|
error: 'InvalidSwap',
|
|
})
|
|
})
|
|
|
|
it('deleteRecord succeeds on proper commit cas', async () => {
|
|
const { repo, sync } = aliceAgent.api.com.atproto
|
|
const { data: post } = await repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: postRecord(),
|
|
})
|
|
const { data: commit } = await sync.getLatestCommit({
|
|
did: aliceAgent.accountDid,
|
|
})
|
|
const uri = new AtUri(post.uri)
|
|
await repo.deleteRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
swapCommit: commit.cid,
|
|
})
|
|
const checkPost = repo.getRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
await expect(checkPost).rejects.toThrow('Could not locate record')
|
|
})
|
|
|
|
it('deleteRecord fails on bad commit cas', async () => {
|
|
const { repo, sync } = aliceAgent.api.com.atproto
|
|
const { data: staleCommit } = await sync.getLatestCommit({
|
|
did: aliceAgent.accountDid,
|
|
})
|
|
const { data: post } = await repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: postRecord(),
|
|
})
|
|
const uri = new AtUri(post.uri)
|
|
const attemptDelete = repo.deleteRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
swapCommit: staleCommit.cid,
|
|
})
|
|
await expect(attemptDelete).rejects.toMatchObject({
|
|
error: 'InvalidSwap',
|
|
})
|
|
const checkPost = repo.getRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
await expect(checkPost).resolves.toBeDefined()
|
|
})
|
|
|
|
it('deleteRecord succeeds on proper record cas', async () => {
|
|
const { repo } = aliceAgent.api.com.atproto
|
|
const { data: post } = await repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: postRecord(),
|
|
})
|
|
const uri = new AtUri(post.uri)
|
|
await repo.deleteRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
swapRecord: post.cid,
|
|
})
|
|
const checkPost = repo.getRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
await expect(checkPost).rejects.toThrow('Could not locate record')
|
|
})
|
|
|
|
it('deleteRecord fails on bad record cas', async () => {
|
|
const { repo } = aliceAgent.api.com.atproto
|
|
const { data: post } = await repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: postRecord(),
|
|
})
|
|
const uri = new AtUri(post.uri)
|
|
const attemptDelete = repo.deleteRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
swapRecord: (await cidForCbor({})).toString(),
|
|
})
|
|
await expect(attemptDelete).rejects.toMatchObject({
|
|
error: 'InvalidSwap',
|
|
})
|
|
const checkPost = repo.getRecord({
|
|
repo: uri.host,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
})
|
|
await expect(checkPost).resolves.toBeDefined()
|
|
})
|
|
|
|
it('putRecord succeeds on proper commit cas', async () => {
|
|
const { repo, sync } = aliceAgent.api.com.atproto
|
|
const { data: commit } = await sync.getLatestCommit({
|
|
did: aliceAgent.accountDid,
|
|
})
|
|
const { data: profile } = await repo.putRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: 'self',
|
|
swapCommit: commit.cid,
|
|
record: profileRecord(),
|
|
})
|
|
const { data: checkProfile } = await repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: 'self',
|
|
})
|
|
expect(checkProfile.cid).toEqual(profile.cid)
|
|
})
|
|
|
|
it('putRecord fails on bad commit cas', async () => {
|
|
const { repo, sync } = aliceAgent.api.com.atproto
|
|
const { data: staleCommit } = await sync.getLatestCommit({
|
|
did: aliceAgent.accountDid,
|
|
})
|
|
// Update repo, change head
|
|
await repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: postRecord(),
|
|
})
|
|
const attemptPut = repo.putRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: 'self',
|
|
swapCommit: staleCommit.cid,
|
|
record: profileRecord(),
|
|
})
|
|
await expect(attemptPut).rejects.toMatchObject({ error: 'InvalidSwap' })
|
|
})
|
|
|
|
it('putRecord succeeds on proper record cas', async () => {
|
|
const { repo } = aliceAgent.api.com.atproto
|
|
// Start with missing profile record, to test swapRecord=null
|
|
await repo.deleteRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: 'self',
|
|
})
|
|
// Test swapRecord w/ null (ensures create)
|
|
const { data: profile1 } = await repo.putRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: 'self',
|
|
swapRecord: null,
|
|
record: profileRecord(),
|
|
})
|
|
const { data: checkProfile1 } = await repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: 'self',
|
|
})
|
|
expect(checkProfile1.cid).toEqual(profile1.cid)
|
|
// Test swapRecord w/ cid (ensures update)
|
|
const { data: profile2 } = await repo.putRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: 'self',
|
|
swapRecord: profile1.cid,
|
|
record: profileRecord(),
|
|
})
|
|
const { data: checkProfile2 } = await repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: 'self',
|
|
})
|
|
expect(checkProfile2.cid).toEqual(profile2.cid)
|
|
})
|
|
|
|
it('putRecord fails on bad record cas', async () => {
|
|
const { repo } = aliceAgent.api.com.atproto
|
|
// Test swapRecord w/ null (ensures create)
|
|
const attemptPut1 = repo.putRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: 'self',
|
|
swapRecord: null,
|
|
record: profileRecord(),
|
|
})
|
|
await expect(attemptPut1).rejects.toMatchObject({ error: 'InvalidSwap' })
|
|
// Test swapRecord w/ cid (ensures update)
|
|
const attemptPut2 = repo.putRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyActorProfile,
|
|
rkey: 'self',
|
|
swapRecord: (await cidForCbor({})).toString(),
|
|
record: profileRecord(),
|
|
})
|
|
await expect(attemptPut2).rejects.toMatchObject({ error: 'InvalidSwap' })
|
|
})
|
|
|
|
it('applyWrites succeeds on proper commit cas', async () => {
|
|
const { repo, sync } = aliceAgent.api.com.atproto
|
|
const { data: commit } = await sync.getLatestCommit({
|
|
did: aliceAgent.accountDid,
|
|
})
|
|
await repo.applyWrites({
|
|
repo: aliceAgent.accountDid,
|
|
swapCommit: commit.cid,
|
|
writes: [
|
|
{
|
|
$type: `${ids.ComAtprotoRepoApplyWrites}#create`,
|
|
action: 'create',
|
|
collection: ids.AppBskyFeedPost,
|
|
value: { $type: ids.AppBskyFeedPost, ...postRecord() },
|
|
},
|
|
],
|
|
})
|
|
})
|
|
|
|
it('applyWrites fails on bad commit cas', async () => {
|
|
const { repo, sync } = aliceAgent.api.com.atproto
|
|
const { data: staleCommit } = await sync.getLatestCommit({
|
|
did: aliceAgent.accountDid,
|
|
})
|
|
// Update repo, change head
|
|
await repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: ids.AppBskyFeedPost,
|
|
record: postRecord(),
|
|
})
|
|
const attemptApplyWrite = repo.applyWrites({
|
|
repo: aliceAgent.accountDid,
|
|
swapCommit: staleCommit.cid,
|
|
writes: [
|
|
{
|
|
$type: `${ids.ComAtprotoRepoApplyWrites}#create`,
|
|
action: 'create',
|
|
collection: ids.AppBskyFeedPost,
|
|
value: { $type: ids.AppBskyFeedPost, ...postRecord() },
|
|
},
|
|
],
|
|
})
|
|
await expect(attemptApplyWrite).rejects.toMatchObject({
|
|
error: 'InvalidSwap',
|
|
})
|
|
})
|
|
|
|
it("writes fail on values that can't reliably transform between cbor to lex", async () => {
|
|
const passthroughBody = (data: unknown) =>
|
|
ui8ToArrayBuffer(new TextEncoder().encode(JSON.stringify(data)))
|
|
|
|
const result = aliceAgent.call(
|
|
'com.atproto.repo.createRecord',
|
|
{},
|
|
passthroughBody({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.post',
|
|
record: {
|
|
text: 'x',
|
|
createdAt: new Date().toISOString(),
|
|
deepObject: createDeepObject(3000),
|
|
},
|
|
}),
|
|
{
|
|
encoding: 'application/json',
|
|
},
|
|
)
|
|
|
|
await expect(result).rejects.toMatchObject({
|
|
status: 400,
|
|
error: 'InvalidRequest',
|
|
message: 'Bad record',
|
|
})
|
|
})
|
|
})
|
|
|
|
it('prevents duplicate likes', async () => {
|
|
const now = new Date().toISOString()
|
|
const uriA = AtUri.make(
|
|
bobAgent.accountDid,
|
|
'app.bsky.feed.post',
|
|
TID.nextStr(),
|
|
)
|
|
const cidA = await cidForCbor({ post: 'a' })
|
|
const uriB = AtUri.make(
|
|
bobAgent.accountDid,
|
|
'app.bsky.feed.post',
|
|
TID.nextStr(),
|
|
)
|
|
const cidB = await cidForCbor({ post: 'b' })
|
|
|
|
const { data: like1 } = await aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.like',
|
|
record: {
|
|
$type: 'app.bsky.feed.like',
|
|
subject: { uri: uriA.toString(), cid: cidA.toString() },
|
|
createdAt: now,
|
|
},
|
|
})
|
|
const { data: like2 } = await aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.like',
|
|
record: {
|
|
$type: 'app.bsky.feed.like',
|
|
subject: { uri: uriB.toString(), cid: cidB.toString() },
|
|
createdAt: now,
|
|
},
|
|
})
|
|
const { data: like3 } = await aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.like',
|
|
record: {
|
|
$type: 'app.bsky.feed.like',
|
|
subject: { uri: uriA.toString(), cid: cidA.toString() },
|
|
createdAt: now,
|
|
},
|
|
})
|
|
|
|
const getLike1 = aliceAgent.api.com.atproto.repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.like',
|
|
rkey: new AtUri(like1.uri).rkey,
|
|
})
|
|
|
|
await expect(getLike1).rejects.toThrow('Could not locate record:')
|
|
|
|
const getLike2 = aliceAgent.api.com.atproto.repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.like',
|
|
rkey: new AtUri(like2.uri).rkey,
|
|
})
|
|
|
|
await expect(getLike2).resolves.toBeDefined()
|
|
|
|
const getLike3 = aliceAgent.api.com.atproto.repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.like',
|
|
rkey: new AtUri(like3.uri).rkey,
|
|
})
|
|
|
|
await expect(getLike3).resolves.toBeDefined()
|
|
})
|
|
|
|
it('prevents duplicate reposts', async () => {
|
|
const now = new Date().toISOString()
|
|
const uriA = AtUri.make(
|
|
bobAgent.accountDid,
|
|
'app.bsky.feed.post',
|
|
TID.nextStr(),
|
|
)
|
|
const cidA = await cidForCbor({ post: 'a' })
|
|
const uriB = AtUri.make(
|
|
bobAgent.accountDid,
|
|
'app.bsky.feed.post',
|
|
TID.nextStr(),
|
|
)
|
|
const cidB = await cidForCbor({ post: 'b' })
|
|
|
|
const { data: repost1 } =
|
|
await aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.repost',
|
|
record: {
|
|
$type: 'app.bsky.feed.repost',
|
|
subject: { uri: uriA.toString(), cid: cidA.toString() },
|
|
createdAt: now,
|
|
},
|
|
})
|
|
const { data: repost2 } =
|
|
await aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.repost',
|
|
record: {
|
|
$type: 'app.bsky.feed.repost',
|
|
subject: { uri: uriB.toString(), cid: cidB.toString() },
|
|
createdAt: now,
|
|
},
|
|
})
|
|
const { data: repost3 } =
|
|
await aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.repost',
|
|
record: {
|
|
$type: 'app.bsky.feed.repost',
|
|
subject: { uri: uriA.toString(), cid: cidA.toString() },
|
|
createdAt: now,
|
|
},
|
|
})
|
|
|
|
const getRepost1 = aliceAgent.api.com.atproto.repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.repost',
|
|
rkey: new AtUri(repost1.uri).rkey,
|
|
})
|
|
|
|
await expect(getRepost1).rejects.toThrow('Could not locate record:')
|
|
|
|
const getRepost2 = aliceAgent.api.com.atproto.repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.repost',
|
|
rkey: new AtUri(repost2.uri).rkey,
|
|
})
|
|
|
|
await expect(getRepost2).resolves.toBeDefined()
|
|
|
|
const getRepost3 = aliceAgent.api.com.atproto.repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.feed.repost',
|
|
rkey: new AtUri(repost3.uri).rkey,
|
|
})
|
|
|
|
await expect(getRepost3).resolves.toBeDefined()
|
|
})
|
|
|
|
it('prevents duplicate blocks', async () => {
|
|
const now = new Date().toISOString()
|
|
|
|
const { data: block1 } = await aliceAgent.api.com.atproto.repo.createRecord(
|
|
{
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.graph.block',
|
|
record: {
|
|
$type: 'app.bsky.graph.block',
|
|
subject: bobAgent.accountDid,
|
|
createdAt: now,
|
|
},
|
|
},
|
|
)
|
|
|
|
const { data: block2 } = await bobAgent.api.com.atproto.repo.createRecord({
|
|
repo: bobAgent.accountDid,
|
|
collection: 'app.bsky.graph.block',
|
|
record: {
|
|
$type: 'app.bsky.graph.block',
|
|
subject: aliceAgent.accountDid,
|
|
createdAt: now,
|
|
},
|
|
})
|
|
|
|
const { data: block3 } = await aliceAgent.api.com.atproto.repo.createRecord(
|
|
{
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.graph.block',
|
|
record: {
|
|
$type: 'app.bsky.graph.block',
|
|
subject: bobAgent.accountDid,
|
|
createdAt: now,
|
|
},
|
|
},
|
|
)
|
|
|
|
const getBlock1 = aliceAgent.api.com.atproto.repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.graph.block',
|
|
rkey: new AtUri(block1.uri).rkey,
|
|
})
|
|
|
|
await expect(getBlock1).rejects.toThrow('Could not locate record:')
|
|
|
|
const getBlock2 = aliceAgent.api.com.atproto.repo.getRecord({
|
|
repo: bobAgent.accountDid,
|
|
collection: 'app.bsky.graph.block',
|
|
rkey: new AtUri(block2.uri).rkey,
|
|
})
|
|
|
|
await expect(getBlock2).resolves.toBeDefined()
|
|
|
|
const getBlock3 = aliceAgent.api.com.atproto.repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.graph.block',
|
|
rkey: new AtUri(block3.uri).rkey,
|
|
})
|
|
|
|
await expect(getBlock3).resolves.toBeDefined()
|
|
})
|
|
|
|
it('prevents duplicate follows', async () => {
|
|
const now = new Date().toISOString()
|
|
|
|
const { data: follow1 } =
|
|
await aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.graph.follow',
|
|
record: {
|
|
$type: 'app.bsky.graph.follow',
|
|
subject: bobAgent.accountDid,
|
|
createdAt: now,
|
|
},
|
|
})
|
|
const { data: follow2 } = await bobAgent.api.com.atproto.repo.createRecord({
|
|
repo: bobAgent.accountDid,
|
|
collection: 'app.bsky.graph.follow',
|
|
record: {
|
|
$type: 'app.bsky.graph.follow',
|
|
subject: aliceAgent.accountDid,
|
|
createdAt: now,
|
|
},
|
|
})
|
|
const { data: follow3 } =
|
|
await aliceAgent.api.com.atproto.repo.createRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.graph.follow',
|
|
record: {
|
|
$type: 'app.bsky.graph.follow',
|
|
subject: bobAgent.accountDid,
|
|
createdAt: now,
|
|
},
|
|
})
|
|
|
|
const getFollow1 = aliceAgent.api.com.atproto.repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.graph.follow',
|
|
rkey: new AtUri(follow1.uri).rkey,
|
|
})
|
|
|
|
await expect(getFollow1).rejects.toThrow('Could not locate record:')
|
|
|
|
const getFollow2 = aliceAgent.api.com.atproto.repo.getRecord({
|
|
repo: bobAgent.accountDid,
|
|
collection: 'app.bsky.graph.follow',
|
|
rkey: new AtUri(follow2.uri).rkey,
|
|
})
|
|
|
|
await expect(getFollow2).resolves.toBeDefined()
|
|
|
|
const getFollow3 = aliceAgent.api.com.atproto.repo.getRecord({
|
|
repo: aliceAgent.accountDid,
|
|
collection: 'app.bsky.graph.follow',
|
|
rkey: new AtUri(follow3.uri).rkey,
|
|
})
|
|
|
|
await expect(getFollow3).resolves.toBeDefined()
|
|
})
|
|
|
|
// Moderation
|
|
// --------------
|
|
|
|
it("doesn't serve taken-down record", async () => {
|
|
const created = await aliceAgent.app.bsky.feed.post.create(
|
|
{ repo: aliceAgent.accountDid },
|
|
{
|
|
$type: 'app.bsky.feed.post',
|
|
text: 'Hello, world!',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
)
|
|
const postUri = new AtUri(created.uri)
|
|
const post = await agent.app.bsky.feed.post.get({
|
|
repo: aliceAgent.accountDid,
|
|
rkey: postUri.rkey,
|
|
})
|
|
const posts = await agent.app.bsky.feed.post.list({
|
|
repo: aliceAgent.accountDid,
|
|
})
|
|
expect(posts.records.map((r) => r.uri)).toContain(post.uri)
|
|
|
|
const subject = {
|
|
$type: 'com.atproto.repo.strongRef',
|
|
uri: created.uri,
|
|
cid: created.cid,
|
|
}
|
|
await agent.api.com.atproto.admin.updateSubjectStatus(
|
|
{
|
|
subject,
|
|
takedown: { applied: true },
|
|
},
|
|
{
|
|
encoding: 'application/json',
|
|
headers: { authorization: network.pds.adminAuth() },
|
|
},
|
|
)
|
|
|
|
const postTakedownPromise = agent.app.bsky.feed.post.get({
|
|
repo: aliceAgent.accountDid,
|
|
rkey: postUri.rkey,
|
|
})
|
|
await expect(postTakedownPromise).rejects.toThrow('Could not locate record')
|
|
const postsTakedown = await agent.app.bsky.feed.post.list({
|
|
repo: aliceAgent.accountDid,
|
|
})
|
|
expect(postsTakedown.records.map((r) => r.uri)).not.toContain(post.uri)
|
|
|
|
// Cleanup
|
|
await agent.api.com.atproto.admin.updateSubjectStatus(
|
|
{
|
|
subject,
|
|
takedown: { applied: false },
|
|
},
|
|
{
|
|
encoding: 'application/json',
|
|
headers: { authorization: network.pds.adminAuth() },
|
|
},
|
|
)
|
|
})
|
|
|
|
it("doesn't serve taken-down actor", async () => {
|
|
const posts = await agent.app.bsky.feed.post.list({
|
|
repo: aliceAgent.accountDid,
|
|
})
|
|
expect(posts.records.length).toBeGreaterThan(0)
|
|
|
|
const subject = {
|
|
$type: 'com.atproto.admin.defs#repoRef',
|
|
did: aliceAgent.accountDid,
|
|
}
|
|
|
|
await agent.api.com.atproto.admin.updateSubjectStatus(
|
|
{
|
|
subject,
|
|
takedown: { applied: true },
|
|
},
|
|
{
|
|
encoding: 'application/json',
|
|
headers: { authorization: network.pds.adminAuth() },
|
|
},
|
|
)
|
|
|
|
const tryListPosts = agent.app.bsky.feed.post.list({
|
|
repo: aliceAgent.accountDid,
|
|
})
|
|
await expect(tryListPosts).rejects.toThrow(/Could not find repo/)
|
|
|
|
// Cleanup
|
|
await agent.api.com.atproto.admin.updateSubjectStatus(
|
|
{
|
|
subject,
|
|
takedown: { applied: false },
|
|
},
|
|
{
|
|
encoding: 'application/json',
|
|
headers: { authorization: network.pds.adminAuth() },
|
|
},
|
|
)
|
|
})
|
|
})
|
|
|
|
function createDeepObject(depth: number) {
|
|
const obj: any = {}
|
|
let iter = obj
|
|
for (let i = 0; i < depth; ++i) {
|
|
iter.x = {}
|
|
iter = iter.x
|
|
}
|
|
return obj
|
|
}
|