OverviewFilesCommitsIssuesDocsReleasesPeers

favela-bridge / test / profile.test.js

84 lines · javascript ref:
// The bsky profile projection. Pure, so all of this runs with no repo, no socket and no network.
import test from 'brittle'
import { planProfile, sameProfile, MAX_NAME_GRAPHEMES, MAX_DESCRIPTION_GRAPHEMES } from '../lib/profile.js'

const png = (oid) => ({ oid, mimeType: 'image/png', size: 100 })

test('a full profile projects every field', (t) => {
  const p = planProfile({ name: 'ash', bio: 'downstairs device', avatar: png('a'), banner: png('b') })
  t.ok(p.project)
  t.is(p.record.$type, 'app.bsky.actor.profile')
  t.is(p.record.displayName, 'ash')
  t.is(p.record.description, 'downstairs device')
  t.alike(Object.keys(p.images).sort(), ['avatar', 'banner'], 'both pictures named for upload')
  t.is(p.losses.length, 0)
})

// ⛔ The same rule plan.js applies to a post: never publish something the author did not write.
test('an over-long field is OMITTED and reported, and the rest still goes', (t) => {
  const p = planProfile({ name: 'x'.repeat(MAX_NAME_GRAPHEMES + 1), bio: 'fine' })
  t.absent(p.record.displayName, 'the name is not truncated to fit')
  t.is(p.record.description, 'fine', 'and the rest of the profile is unaffected')
  t.ok(p.losses.some((l) => l.includes('display name omitted')), 'and the loss is named')
  t.ok(p.project, 'a profile with one usable field is still worth writing')

  const q = planProfile({ bio: 'y'.repeat(MAX_DESCRIPTION_GRAPHEMES + 1) })
  t.absent(q.record.description)
  t.ok(q.losses.some((l) => l.includes('description omitted')))
})

test('★ an emoji is ONE grapheme, not four bytes', (t) => {
  const p = planProfile({ name: '🌮'.repeat(MAX_NAME_GRAPHEMES) })
  t.ok(p.record.displayName, 'counted as graphemes — a byte count would reject this')
})

// ⛔ favela accepts gif and webp; bsky's lexicon does not.
test('a picture bsky will not take is omitted rather than converted', (t) => {
  const p = planProfile({ name: 'ash', avatar: { oid: 'a', mimeType: 'image/webp', size: 10 } })
  t.absent(p.images.avatar, 'not offered for upload')
  t.ok(p.losses.some((l) => l.includes('avatar omitted')), 'and said so')
  t.is(p.record.displayName, 'ash', 'the rest of the profile still projects')
})

// ⛔ A projection must never erase.
test('⛔ an empty profile projects NOTHING rather than an empty record', (t) => {
  const p = planProfile({})
  t.absent(p.project, 'writing {$type} alone would replace what is there with nothing')
  const q = planProfile({ name: '   ', bio: '' })
  t.absent(q.project, 'and whitespace is not content')
})

// ⛔ Reconciliation compares against the REPO, not a local note of what we last sent.
test('sameProfile compares blob refs by CID', (t) => {
  const desired = { displayName: 'ash', avatar: { $type: 'blob', ref: { $link: 'bafk1' }, mimeType: 'image/png', size: 1 } }
  const actual = { displayName: 'ash', avatar: { $type: 'blob', ref: { $link: 'bafk1' }, mimeType: 'image/png', size: 1 } }
  t.ok(sameProfile(desired, actual), 'identical content-addressed picture is the same picture')

  const changed = { displayName: 'ash', avatar: { $type: 'blob', ref: { $link: 'bafk2' }, mimeType: 'image/png', size: 1 } }
  t.absent(sameProfile(desired, changed), 'a different CID is a different picture')
  t.absent(sameProfile(desired, { displayName: 'ashatron' }), 'a changed name is a change')
  t.absent(sameProfile(desired, null), 'nothing published yet is not "the same"')
})

test('adding a banner to a profile that had none is a change', (t) => {
  const before = { displayName: 'ash' }
  const after = { displayName: 'ash', banner: { ref: { $link: 'bafk9' } } }
  t.absent(sameProfile(after, before), '⛔ or the first banner would never be written')
})

// ⛔ A bsky description is PLAIN TEXT and the bio is markdown.
test('the description is stripped, not handed over raw', (t) => {
  const p = planProfile({ bio: 'the **dev** box, and a [link](https://example.com)' })
  t.absent(p.record.description.includes('**'), '⛔ or the asterisks show on the live profile')
  t.ok(p.record.description.includes('dev'), 'the words survive')
})

// ⛔ A profile picture caps at 1,000,000 — HALF the post-embed ceiling, and there was no gate at all.
test('an oversized profile picture is omitted rather than sent to be refused', (t) => {
  const p = planProfile({ name: 'ash', avatar: { oid: 'a', mimeType: 'image/png', size: 1000001 } })
  t.absent(p.images.avatar, 'not offered for upload')
  t.ok(p.losses.some((l) => l.includes('profile limit')), 'and named')
  const ok = planProfile({ name: 'ash', avatar: { oid: 'a', mimeType: 'image/png', size: 999999 } })
  t.ok(ok.images.avatar, 'and one that fits still goes')
})