OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / test / unit / server-sync.test.js

175 lines · javascript ref:
import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import { ServerSync } from '../../lib/p2p/federation/server-sync.js'

const tmpDir = path.join(os.tmpdir(), 'pear-git-test-server-sync-' + Date.now())

test('setup: create temp dir', async (t) => {
  await fs.promises.mkdir(tmpDir, { recursive: true })
  t.pass('temp dir created')
})

test('constructor: sets defaults', (t) => {
  const mockRegistry = {}
  const sync = new ServerSync({
    reposDir: '/tmp/repos',
    registry: mockRegistry
  })

  t.is(sync.reposDir, '/tmp/repos')
  t.is(sync.registry, mockRegistry)
  t.is(sync.interval, 30 * 60 * 1000, 'default interval is 30 minutes')
  t.is(sync._timer, null)
  t.is(sync._running, false)
})

test('constructor: accepts custom interval', (t) => {
  const sync = new ServerSync({
    reposDir: '/tmp/repos',
    registry: {},
    interval: 30000
  })

  t.is(sync.interval, 30000)
})

test('_listRepos: returns repo names from .git directories', async (t) => {
  const reposDir = path.join(tmpDir, 'repos')
  await fs.promises.mkdir(reposDir, { recursive: true })

  // Create some .git dirs and a non-.git dir
  await fs.promises.mkdir(path.join(reposDir, 'my-app.git'), { recursive: true })
  await fs.promises.mkdir(path.join(reposDir, 'another-repo.git'), { recursive: true })
  await fs.promises.mkdir(path.join(reposDir, 'not-a-repo'), { recursive: true })

  const sync = new ServerSync({
    reposDir,
    registry: {}
  })

  const repos = await sync._listRepos()
  t.is(repos.length, 2)
  t.ok(repos.includes('my-app'))
  t.ok(repos.includes('another-repo'))
  t.absent(repos.includes('not-a-repo'))
})

test('_listRepos: returns empty array for missing directory', async (t) => {
  const sync = new ServerSync({
    reposDir: path.join(tmpDir, 'nonexistent'),
    registry: {}
  })

  const repos = await sync._listRepos()
  t.alike(repos, [])
})

test('_buildSyncList: returns active peers with httpUrl', async (t) => {
  // Create a mock registry with createReadStream
  const entries = [
    {
      key: 'federation:abc123',
      value: { serverKey: 'abc12345', status: 'active', httpUrl: 'http://peer1:5005/git' }
    },
    {
      key: 'federation:def456',
      value: { serverKey: 'def45678', status: 'inactive', httpUrl: 'http://peer2:5005/git' }
    },
    {
      key: 'federation:ghi789',
      value: { serverKey: 'ghi78901', status: 'active', httpUrl: 'http://peer3:5005/git' }
    },
    {
      key: 'federation:jkl012',
      value: { serverKey: 'jkl01234', status: 'active' } // no httpUrl
    }
  ]

  const mockRegistry = {
    createReadStream () {
      return (async function * () {
        for (const entry of entries) {
          yield entry
        }
      })()
    }
  }

  const sync = new ServerSync({
    reposDir: tmpDir,
    registry: mockRegistry
  })

  const peers = await sync._buildSyncList()
  t.is(peers.length, 3, 'active peers with serverKey or httpUrl')
  t.is(peers[0].serverKey, 'abc12345')
  t.is(peers[1].serverKey, 'ghi78901')
  t.is(peers[2].serverKey, 'jkl01234')
})

test('_buildSyncList: returns empty for no entries', async (t) => {
  const mockRegistry = {
    createReadStream () {
      return (async function * () {})()
    }
  }

  const sync = new ServerSync({
    reposDir: tmpDir,
    registry: mockRegistry
  })

  const peers = await sync._buildSyncList()
  t.alike(peers, [])
})

test('_buildSyncList: handles string JSON values', async (t) => {
  const mockRegistry = {
    createReadStream () {
      return (async function * () {
        yield {
          key: 'federation:str1',
          value: JSON.stringify({ serverKey: 'str12345', status: 'active', httpUrl: 'http://str-peer:5005' })
        }
      })()
    }
  }

  const sync = new ServerSync({
    reposDir: tmpDir,
    registry: mockRegistry
  })

  const peers = await sync._buildSyncList()
  t.is(peers.length, 1)
  t.is(peers[0].serverKey, 'str12345')
})

test('start/stop: manages timer lifecycle', (t) => {
  const sync = new ServerSync({
    reposDir: tmpDir,
    registry: {},
    interval: 60000
  })

  t.is(sync._timer, null)

  sync.start()
  t.not(sync._timer, null, 'timer set after start')

  // Starting again should be a no-op
  const timer1 = sync._timer
  sync.start()
  t.is(sync._timer, timer1, 'start is idempotent')

  sync.stop()
  t.is(sync._timer, null, 'timer cleared after stop')
})

test('cleanup: remove temp dir', async (t) => {
  await fs.promises.rm(tmpDir, { recursive: true, force: true })
  t.pass('cleaned up')
})