OverviewFilesCommitsIssuesDocsReleasesPeers

pearbrowser-desktop / test / shield-list-sync.test.js

184 lines · javascript ref:
import test from 'node:test'
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { createRequire } from 'node:module'

const require = createRequire(import.meta.url)
const { ContentShield } = require('../backend/content-shield.cjs')
const { ShieldListSync, ShieldListSyncError, DRIVE_LIST_PREFIX, MAX_LIST_BYTES } = require('../backend/shield-list-sync.cjs')

const KEY = 'c'.repeat(64)
const sha256Hex = (buf) => createHash('sha256').update(buf).digest('hex')

function fakeDrive (files) {
  const calls = []
  return {
    calls,
    files,
    fetch: async (driveKey, path) => {
      calls.push(`${driveKey.slice(0, 4)}:${path}`)
      const value = files[path]
      return value == null ? null : { content: Buffer.from(value) }
    }
  }
}

function makeSync ({ files, shield } = {}) {
  const persisted = []
  const refreshes = []
  const drive = fakeDrive(files || {})
  const engine = shield || new ContentShield({ builtinList: false })
  const sync = new ShieldListSync({
    shield: engine,
    fetchDriveFile: drive.fetch,
    refreshDrive: async (driveKey) => { refreshes.push(driveKey) },
    sha256Hex,
    persistMeta: async (meta) => { persisted.push(structuredClone(meta)) },
    now: () => 1234
  })
  return { sync, shield: engine, drive, persisted, refreshes }
}

test('subscribe fetches, verifies, registers, and persists a drive list', async () => {
  const filters = '||ads.example.com^\n##.promo\n'
  const { sync, shield, persisted, refreshes } = makeSync({
    files: {
      '/manifest.json': JSON.stringify({ name: 'pear-default', version: '3', sha256: sha256Hex(filters) }),
      '/filters.txt': filters
    }
  })

  const result = await sync.subscribe(KEY)
  assert.deepEqual(refreshes, [KEY])
  assert.equal(result.changed, true)
  assert.equal(result.name, 'pear-default')
  assert.equal(result.version, '3')
  assert.ok(result.rules >= 2)

  assert.equal(shield.shouldBlockUrl('https://ads.example.com/x.js').blocked, true)
  assert.ok(shield.stats().lists.includes(DRIVE_LIST_PREFIX + KEY))
  assert.equal(persisted.length, 1)
  assert.equal(persisted[0][KEY].version, '3')
  assert.deepEqual(sync.subscriptions().map(item => item.driveKey), [KEY])
})

test('a checksum mismatch fails closed: nothing registered', async () => {
  const { sync, shield } = makeSync({
    files: {
      '/manifest.json': JSON.stringify({ version: '1', sha256: 'f'.repeat(64) }),
      '/filters.txt': '||ads.example.com^'
    }
  })

  await assert.rejects(sync.subscribe(KEY), (err) => {
    assert.ok(err instanceof ShieldListSyncError)
    assert.equal(err.code, 'checksum-mismatch')
    return true
  })
  assert.equal(shield.shouldBlockUrl('https://ads.example.com/x.js').blocked, false)
  assert.equal(sync.subscriptions().length, 0)
})

test('a missing or oversized list fails closed', async () => {
  const { sync } = makeSync({ files: {} })
  await assert.rejects(sync.subscribe(KEY), err => err.code === 'list-unavailable')

  const big = makeSync({ files: { '/filters.txt': 'a'.repeat(MAX_LIST_BYTES + 1) } })
  await assert.rejects(big.sync.subscribe(KEY), err => err.code === 'list-too-large')
})

test('refresh skips the download when version and checksum are unchanged, hot-swaps on change', async () => {
  const v1 = '||ads.example.com^'
  const files = {
    '/manifest.json': JSON.stringify({ version: '1', sha256: sha256Hex(v1) }),
    '/filters.txt': v1
  }
  const { sync, shield, drive } = makeSync({ files })
  await sync.subscribe(KEY)

  const callsAfterSubscribe = drive.calls.length
  const unchanged = await sync.refresh(KEY)
  assert.equal(unchanged.changed, false)
  // Only the manifest was consulted — no rules re-download on same version.
  assert.deepEqual(drive.calls.slice(callsAfterSubscribe), [`${KEY.slice(0, 4)}:/manifest.json`])

  const v2 = '||ads.example.com^\n||tracker.example.net^'
  files['/manifest.json'] = JSON.stringify({ version: '2', sha256: sha256Hex(v2) })
  files['/filters.txt'] = v2
  const changed = await sync.refresh(KEY)
  assert.equal(changed.changed, true)
  assert.equal(changed.version, '2')
  assert.equal(shield.shouldBlockUrl('https://tracker.example.net/p.gif').blocked, true)
})

test('restore + shield state give offline boot without any fetch', async () => {
  // First session: subscribe and capture durable state.
  const filters = '||ads.example.com^'
  const first = makeSync({
    files: {
      '/manifest.json': JSON.stringify({ name: 'pear-default', version: '5', sha256: sha256Hex(filters) }),
      '/filters.txt': filters
    }
  })
  await first.sync.subscribe(KEY)
  const durableShieldState = first.shield.exportListState()
  const durableMeta = first.persisted.at(-1)

  // Second session: no network at all — restore from durable state only.
  const offlineShield = new ContentShield({ builtinList: false })
  offlineShield.importListState(durableShieldState)
  const offline = makeSync({ files: {}, shield: offlineShield })
  const restored = offline.sync.restore(durableMeta)

  assert.equal(restored, 1)
  assert.equal(offline.sync.isSubscribed(KEY), true)
  assert.equal(offline.sync.subscriptions()[0].version, '5')
  assert.equal(offlineShield.shouldBlockUrl('https://ads.example.com/x.js').blocked, true)
  assert.deepEqual(offline.drive.calls, [])
})

test('unsubscribe removes the rules and the durable metadata', async () => {
  const filters = '||ads.example.com^'
  const { sync, shield, persisted } = makeSync({
    files: { '/filters.txt': filters }
  })
  await sync.subscribe(KEY)
  assert.equal(shield.shouldBlockUrl('https://ads.example.com/x.js').blocked, true)

  const removed = await sync.unsubscribe(KEY)
  assert.equal(removed.removed, true)
  assert.equal(shield.shouldBlockUrl('https://ads.example.com/x.js').blocked, false)
  assert.deepEqual(persisted.at(-1), {})
  assert.equal(sync.subscriptions().length, 0)
})

test('refreshAll isolates per-drive failures', async () => {
  const goodKey = 'd'.repeat(64)
  const filters = '||ads.example.com^'
  const files = { '/filters.txt': filters }
  const { sync } = makeSync({ files })
  await sync.subscribe(goodKey)

  // Subscribe a second drive, then break its content for the next sweep.
  const flakyKey = 'e'.repeat(64)
  files['/filters.txt'] = filters
  await sync.subscribe(flakyKey)
  const originalFetch = sync._fetch
  sync._fetch = async (key, path) => {
    if (key === flakyKey) throw new Error('drive offline')
    return originalFetch(key, path)
  }

  const outcomes = await sync.refreshAll({ force: true })
  const byKey = Object.fromEntries(outcomes.map(item => [item.driveKey, item]))
  assert.equal(byKey[goodKey].ok, true)
  assert.equal(byKey[flakyKey].ok, false)
})

test('invalid keys and duplicate list names are rejected', async () => {
  const { sync } = makeSync({ files: {} })
  await assert.rejects(sync.subscribe('not-a-key'), err => err.code === 'invalid-drive-key')
  await assert.rejects(sync.refresh('a'.repeat(64)), err => err.code === 'not-subscribed')
  assert.equal((await sync.unsubscribe('b'.repeat(64))).removed, false)
})