Files
2026-03-23 18:10:16 +01:00

301 lines
8.4 KiB
TypeScript

import {
AppBskyDraftCreateDraft,
AppBskyDraftDefs,
AppBskyDraftGetDrafts,
AtpAgent,
ids,
} from '@atproto/api'
import { TID } from '@atproto/common'
import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'
import { paginateAll } from '../_util'
type Database = TestNetwork['bsky']['db']
const LIMIT = 10
describe('appview drafts views', () => {
let network: TestNetwork
let agent: AtpAgent
let sc: SeedClient
let db: Database
// account dids, for convenience
let alice: string
let bob: string
beforeAll(async () => {
network = await TestNetwork.create({
dbPostgresSchema: 'bsky_views_drafts',
bsky: {
draftsLimit: LIMIT,
},
})
db = network.bsky.db
agent = network.bsky.getAgent()
sc = network.getSeedClient()
await basicSeed(sc)
await network.processAll()
alice = sc.dids.alice
bob = sc.dids.bob
})
afterEach(async () => {
jest.resetAllMocks()
await clearDrafts(db)
})
afterAll(async () => {
await network.close()
})
const makeDraft = (): AppBskyDraftDefs.Draft => ({
posts: [{ text: 'Hello, world!' }],
})
const get = async (actor: string, limit?: number, cursor?: string) =>
agent.app.bsky.draft.getDrafts(
{ limit, cursor },
{
headers: await network.serviceHeaders(actor, ids.AppBskyDraftGetDrafts),
},
)
const create = async (actor: string, draft: AppBskyDraftDefs.Draft) =>
agent.app.bsky.draft.createDraft(
{ draft },
{
headers: await network.serviceHeaders(
actor,
ids.AppBskyDraftCreateDraft,
),
},
)
const update = async (
actor: string,
draftWithId: AppBskyDraftDefs.DraftWithId,
) =>
agent.app.bsky.draft.updateDraft(
{ draft: draftWithId },
{
headers: await network.serviceHeaders(
actor,
ids.AppBskyDraftUpdateDraft,
),
},
)
const del = async (actor: string, id: string) =>
agent.app.bsky.draft.deleteDraft(
{ id },
{
headers: await network.serviceHeaders(
actor,
ids.AppBskyDraftDeleteDraft,
),
},
)
describe('creation', () => {
it('creates drafts', async () => {
const res1 = await create(alice, makeDraft())
const res2 = await create(alice, makeDraft())
const res3 = await create(alice, makeDraft())
expect(res1.data.id).toBeDefined()
expect(res2.data.id).toBeDefined()
expect(res3.data.id).toBeDefined()
expect(new Set([res1.data.id, res2.data.id, res3.data.id]).size).toBe(3)
await create(bob, makeDraft())
await create(bob, makeDraft())
const { data: dataAlice } = await get(alice)
expect(dataAlice.drafts).toHaveLength(3)
const { data: dataBob } = await get(bob)
expect(dataBob.drafts).toHaveLength(2)
})
it('creates drafts with multiple posts (threads)', async () => {
const draft: AppBskyDraftDefs.Draft = {
posts: [
{ text: 'First post in thread' },
{ text: 'Second post in thread' },
{ text: 'Third post in thread' },
],
}
await create(alice, draft)
const { data } = await get(alice)
expect(data.drafts).toHaveLength(1)
expect(data.drafts[0].draft.posts).toHaveLength(3)
expect(data.drafts[0].draft.posts[0].text).toBe('First post in thread')
expect(data.drafts[0].draft.posts[2].text).toBe('Third post in thread')
})
it('limits the drafts', async () => {
// Consume the limit.
for (let i = 0; i < LIMIT; i++) {
await create(alice, makeDraft())
await network.processAll()
}
// Try to go over the limit.
await expect(create(alice, makeDraft())).rejects.toThrow(
AppBskyDraftCreateDraft.DraftLimitReachedError,
)
})
})
describe('update', () => {
it('updates an existing draft', async () => {
const draft1: AppBskyDraftDefs.Draft = {
posts: [{ text: 'First version' }],
}
await create(alice, draft1)
const { data: data0 } = await get(alice)
expect(data0.drafts).toHaveLength(1)
expect(data0.drafts[0].draft.posts[0].text).toBe('First version')
const draftId = data0.drafts[0].id
const draft2: AppBskyDraftDefs.DraftWithId = {
id: draftId,
draft: { posts: [{ text: 'Updated version' }] },
}
await update(alice, draft2)
const { data: data1 } = await get(alice)
expect(data1.drafts).toHaveLength(1)
expect(data1.drafts[0].draft.posts[0].text).toBe('Updated version')
})
it('silently ignores updates to non-existing drafts', async () => {
const nonExistingDraft: AppBskyDraftDefs.DraftWithId = {
id: TID.nextStr(),
draft: { posts: [{ text: 'This draft does not exist' }] },
}
await update(alice, nonExistingDraft)
const { data } = await get(alice)
expect(data.drafts).toHaveLength(0)
})
})
describe('deletion', () => {
it('removes drafts', async () => {
await create(alice, makeDraft())
await create(alice, makeDraft())
await create(alice, makeDraft())
const { data: dataBefore } = await get(alice)
expect(dataBefore.drafts).toHaveLength(3)
const draft1Id = dataBefore.drafts[0].id
const draft2Id = dataBefore.drafts[1].id
const draft3Id = dataBefore.drafts[2].id
await del(alice, draft1Id)
await del(alice, draft3Id)
const { data: dataAfter } = await get(alice)
expect(dataAfter.drafts).toHaveLength(1)
expect(dataAfter.drafts[0].id).toBe(draft2Id)
})
it('is idempotent', async () => {
await create(alice, makeDraft())
const { data: data0 } = await get(alice)
expect(data0.drafts).toHaveLength(1)
const draftId = data0.drafts[0].id
await del(alice, draftId)
const { data: data1 } = await get(alice)
expect(data1.drafts).toHaveLength(0)
await del(alice, draftId)
const { data: data2 } = await get(alice)
expect(data2.drafts).toHaveLength(0)
})
})
describe('listing', () => {
it('gets empty drafts', async () => {
const { data } = await get(alice)
expect(data.drafts).toHaveLength(0)
})
it('drafts are private to each user', async () => {
await create(alice, makeDraft())
await create(alice, makeDraft())
await create(bob, makeDraft())
const { data: dataAlice } = await get(alice)
expect(dataAlice.drafts).toHaveLength(2)
const { data: dataBob } = await get(bob)
expect(dataBob.drafts).toHaveLength(1)
})
it('includes timestamps', async () => {
const beforeCreate = new Date()
await create(alice, makeDraft())
const afterCreate = new Date()
const { data } = await get(alice)
expect(data.drafts).toHaveLength(1)
const createdAt = new Date(data.drafts[0].createdAt)
expect(createdAt.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime())
expect(createdAt.getTime()).toBeLessThanOrEqual(afterCreate.getTime())
const updatedAt = new Date(data.drafts[0].updatedAt)
expect(updatedAt.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime())
expect(updatedAt.getTime()).toBeLessThanOrEqual(afterCreate.getTime())
})
it('paginates drafts in descending order', async () => {
for (let i = 0; i < 7; i++) {
await create(alice, makeDraft())
}
const results = (out: AppBskyDraftGetDrafts.OutputSchema[]) =>
out.flatMap((res) => res.drafts)
const paginator = async (cursor?: string) => {
const res = await get(alice, 2, cursor)
return res.data
}
const fullRes = await get(alice)
expect(fullRes.data.drafts.length).toBe(7)
const paginatedRes = await paginateAll(paginator)
paginatedRes.forEach((res) =>
expect(res.drafts.length).toBeLessThanOrEqual(2),
)
const full = results([fullRes.data])
const paginated = results(paginatedRes)
// Check items are the same.
const sort = (
a: AppBskyDraftDefs.DraftView,
b: AppBskyDraftDefs.DraftView,
) => (a.id > b.id ? 1 : -1)
expect([...paginated].sort(sort)).toEqual([...full].sort(sort))
// Check pagination ordering (most recent first).
expect(paginated.at(0)?.id).toBe(full.at(0)?.id)
expect(paginated.at(-1)?.id).toBe(full.at(-1)?.id)
})
})
})
const clearDrafts = async (db: Database) => {
await db.db.deleteFrom('draft').execute()
}