OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer / test / post-delete.test.js

93 lines · javascript ref:
// Deleting your own post or reply.
//
// ⛔ A TOMBSTONE, like every other undo here (unfollow, unlike, unrepost) and like keet's own message
// delete: absence is ambiguous in a replicated log, so a peer has to be able to tell "deleted" from
// "not replicated to me yet". And the bridges need the same positive evidence to retract what they
// already published — bsky's record and the fediverse's Note are deleted only for a name that is
// KNOWN deleted, never for one that is merely missing from a listing.
//
// These drive `dispatch`, the thing the socket and the in-process client actually call.

import test from 'brittle'
import process from '#node:process'
import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import { FavelaNode } from '../lib/node.js'
import { dispatch } from '../lib/rpc.js'
import { saveCredential } from '../lib/device.js'
import { identity } from './helpers.js'

const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-postdel-'))
const stubTransport = () => ({ async join () {}, async leave () {}, async destroy () {} })

async function node () {
  process.env.FAVELA_NAMESPACE = 'own'
  const storage = tmp()
  const id = await identity()
  const cred = await id.device()
  saveCredential(storage, cred)
  const n = new FavelaNode(storage, { transport: stubTransport() })
  await n.ready()
  await n.link(cred)
  return n
}

test('★ deleting my post removes it from my posts and records it as deleted', async (t) => {
  const n = await node()
  t.teardown(() => n.close())
  const { feed } = await dispatch(n, 'whoami', {})

  const keep = await dispatch(n, 'post', { body: 'keep me' })
  const oops = await dispatch(n, 'post', { body: 'posted twice by accident' })
  const before = await dispatch(n, 'posts-by', { feed })
  t.ok(before.length === 2 && before.every((p) => p.own === true), 'my rows are marked own — what offers the delete button')

  const r = await dispatch(n, 'post-delete', { name: oops.name })
  t.is(r.deleted, oops.name, 'it says what it deleted')
  t.is(r.collection, 'post')

  const names = (await dispatch(n, 'posts-by', { feed })).map((p) => p.name)
  t.ok(names.includes(keep.name), 'the other post is untouched')
  t.absent(names.includes(oops.name), 'the deleted one is gone from my posts')

  const gone = await dispatch(n, 'deleted-own', {})
  t.alike(gone.post, [oops.name], 'and is listed as POSITIVELY deleted — what a bridge retracts on')
  t.alike(gone.reply, [], 'no replies deleted')
})

test('deleting my reply works the same way', async (t) => {
  const n = await node()
  t.teardown(() => n.close())
  const { feed } = await dispatch(n, 'whoami', {})
  const parent = await dispatch(n, 'post', { body: 'parent' })
  const rep = await dispatch(n, 'reply', { target: { feed, collection: 'post', name: parent.name }, body: 'a reply' })

  const r = await dispatch(n, 'post-delete', { name: rep.name })
  t.is(r.collection, 'reply', 'it found the record among my replies')
  t.alike((await dispatch(n, 'deleted-own', {})).reply, [rep.name])
  const replies = await dispatch(n, 'replies', { target: { feed, collection: 'post', name: parent.name } })
  t.absent(replies.some((x) => x.name === rep.name), 'and it is gone from the thread')
})

test('a name I never wrote is refused, and deleting twice is not an error', async (t) => {
  const n = await node()
  t.teardown(() => n.close())
  await t.exception(dispatch(n, 'post-delete', { name: 'not-a-record-of-mine' }), /no post or reply/, 'nothing to delete is said, not ignored')

  const p = await dispatch(n, 'post', { body: 'once' })
  await dispatch(n, 'post-delete', { name: p.name })
  const again = await dispatch(n, 'post-delete', { name: p.name })
  t.is(again.deleted, p.name, 'a second press finds it already deleted')
  t.ok(again.already, 'and says so')
  t.alike((await dispatch(n, 'deleted-own', {})).post, [p.name], 'still one tombstone')
})

test('a holder of the feed can read the tombstones too — what ap-bridge retracts from', async (t) => {
  const n = await node()
  t.teardown(() => n.close())
  const p = await dispatch(n, 'post', { body: 'gone soon' })
  await dispatch(n, 'post-delete', { name: p.name })
  t.alike(await n.peer.feed.tombstones('post'), [p.name], 'Feed.tombstones(collection) lists deleted names')
})