OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / app-registry-icon.test.js

76 lines · javascript ref:
/**
 * v0.17.0: optional display `icon` on catalog entries (for PearBrowser /
 * catalogue app tiles). Pins: icon surfaces in catalog() for non-blind
 * entries, normalizes correctly (string|null, length-capped), survives a
 * persistence round-trip, and is STRIPPED for blind/redacted drives — a
 * drive-relative icon path would leak the addressKey.
 */

import test from 'brittle'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { AppRegistry } from 'p2p-hiverelay/core/app-registry.js'

test('catalog() surfaces icon for a non-blind entry', (t) => {
  const registry = new AppRegistry(null)
  const appKey = 'a'.repeat(64)
  registry.set(appKey, { type: 'app', appId: 'icon-app', name: 'Icon App', icon: '/assets/icon.png' })
  const entry = registry.catalog()[0]
  t.is(entry.icon, '/assets/icon.png', 'icon surfaced in catalog')
})

test('icon defaults to null when unset, and normalizes junk to null', (t) => {
  const registry = new AppRegistry(null)
  registry.set('b'.repeat(64), { type: 'app', appId: 'no-icon', name: 'No Icon' })
  t.is(registry.catalog()[0].icon, null, 'absent icon is null')

  registry.set('c'.repeat(64), { type: 'app', appId: 'bad-icon', name: 'Bad', icon: 12345 })
  t.is(registry.catalog().find(e => e.id === 'bad-icon').icon, null, 'non-string icon normalized to null')

  registry.set('d'.repeat(64), { type: 'app', appId: 'blank-icon', name: 'Blank', icon: '   ' })
  t.is(registry.catalog().find(e => e.id === 'blank-icon').icon, null, 'whitespace-only icon normalized to null')
})

test('icon is length-capped (512 chars) and trimmed', (t) => {
  const registry = new AppRegistry(null)
  registry.set('e'.repeat(64), { type: 'app', appId: 'long-icon', name: 'Long', icon: '  ' + 'x'.repeat(900) + '  ' })
  const icon = registry.catalog()[0].icon
  t.is(icon.length, 512, 'capped to 512')
  t.absent(icon.startsWith(' '), 'leading whitespace trimmed')
})

test('icon is STRIPPED for blind/redacted entries (no addressKey leak)', (t) => {
  const registry = new AppRegistry(null)
  const appKey = 'f'.repeat(64)
  registry.set(appKey, {
    type: 'app',
    appId: 'blind-app',
    name: 'Secret',
    icon: '/icon.png', // a drive-relative path would expose the addressKey
    blind: true,
    blindContentId: '9'.repeat(64)
  })
  const entry = registry.catalog({ redactPrivate: true })[0]
  t.is(entry.redacted, true, 'blind entry is redacted')
  t.is(entry.icon, null, 'icon stripped for blind drive')
})

test('icon survives a persistence round-trip (reload from disk)', async (t) => {
  const dir = await mkdtemp(join(tmpdir(), 'icon-reg-'))
  t.teardown(() => rm(dir, { recursive: true, force: true }))
  const appKey = '1'.repeat(64)

  const reg1 = new AppRegistry(dir)
  await reg1.load().catch(() => {})
  reg1.set(appKey, { type: 'app', appId: 'persist-icon', name: 'Persist', icon: 'https://example.com/i.png' })
  if (typeof reg1.flush === 'function') await reg1.flush()
  if (typeof reg1.close === 'function') await reg1.close()

  const reg2 = new AppRegistry(dir)
  await reg2.load().catch(() => {})
  const got = reg2.get(appKey)
  t.ok(got, 'entry reloaded')
  t.is(got.icon, 'https://example.com/i.png', 'icon preserved across reload')
})