OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-deploy / test / basic.test.js

164 lines · javascript ref:
import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import crypto from 'hypercore-crypto'
import Hyperswarm from 'hyperswarm'
import { DeployServer, DeployClient } from '../index.js'

// NOTE: the standalone `lib/scanner` module (buildManifest / diffManifest /
// hashFile) was removed — scanning now lives inside DeployServer (walkDir +
// FileIndex). Its unit tests were dropped here; equivalent coverage lives in
// tree-publish / incremental-rescan / manifest-boot.

const tmpdir = os.tmpdir()

function mktemp (prefix) {
  const dir = path.join(tmpdir, 'hyper-deploy-test-' + prefix + '-' + crypto.randomBytes(6).toString('hex'))
  fs.mkdirSync(dir, { recursive: true })
  return dir
}

// ── DeployServer ────────────────────────────────────────────

test('DeployServer lifecycle', async (t) => {
  const dir = mktemp('srv')
  fs.writeFileSync(path.join(dir, 'test.txt'), 'hello')

  const server = new DeployServer(dir)
  await server.ready()

  t.ok(server.manifest, 'has manifest')
  t.is(server.manifest.files.length, 1)
  t.ok(server.topicHex, 'has topic')
  t.is(server.connections, 0)

  await server.close()
  fs.rmSync(dir, { recursive: true })
})

test('DeployServer rescan', async (t) => {
  const dir = mktemp('rescan')
  fs.writeFileSync(path.join(dir, 'a.txt'), 'first')

  const server = new DeployServer(dir)
  await server.ready()
  t.is(server.manifest.files.length, 1)

  fs.writeFileSync(path.join(dir, 'b.txt'), 'second')
  await server.rescan()
  t.is(server.manifest.files.length, 2)

  await server.close()
  fs.rmSync(dir, { recursive: true })
})

test('DeployServer rejects missing directory', async (t) => {
  const server = new DeployServer('/nonexistent/path/xyz')
  try {
    await server.ready()
    t.fail('should have thrown')
  } catch (err) {
    t.ok(err.message.includes('not found'), err.message)
  }
})

// ── DeployClient ────────────────────────────────────────────

test('DeployClient timeout on no connection', async (t) => {
  const client = new DeployClient(crypto.randomBytes(32), mktemp('timeout'), {
    timeout: 500
  })
  await client.ready()

  try {
    await client.pull()
    t.fail('should have timed out')
  } catch (err) {
    t.ok(err.message.includes('timeout'), err.message)
  }

  await client.close()
})

// ── Integration ─────────────────────────────────────────────
// These drive a real two-swarm Hyperswarm DHT transfer. They need a local DHT
// bootstrap (@hyperswarm/testnet) to run deterministically in-process — which
// isn't a dependency here — so they're skipped rather than left to flake on the
// public DHT. Networked transfer is covered by the cross-machine e2e harness.

test.skip('DeployServer + DeployClient full transfer', async (t) => {
  t.timeout(15000)

  const srcDir = mktemp('int-src')
  for (let i = 0; i < 5; i++) {
    fs.writeFileSync(path.join(srcDir, 'file_' + i + '.txt'), 'content ' + i)
  }

  const dstDir = mktemp('int-dst')

  // Separate swarms — same process can't self-connect on one swarm.
  const topic = crypto.randomBytes(32)
  const swarmA = new Hyperswarm()
  const swarmB = new Hyperswarm()

  const server = new DeployServer(srcDir, { topic, swarm: swarmA })
  const client = new DeployClient(topic, dstDir, { swarm: swarmB, timeout: 10000 })

  await server.ready()
  await client.ready()

  const result = await client.pull()

  t.is(result.filesTransferred, 5, '5 files transferred')
  t.is(result.filesSkipped, 0, 'none skipped')
  t.ok(result.bytesReceived > 0, 'received bytes')
  t.is(result.errors.length, 0, 'no errors')

  for (let i = 0; i < 5; i++) {
    const src = fs.readFileSync(path.join(srcDir, 'file_' + i + '.txt'), 'utf8')
    const dst = fs.readFileSync(path.join(dstDir, 'file_' + i + '.txt'), 'utf8')
    t.is(dst, src, 'file_' + i + '.txt matches')
  }

  await client.close()
  await server.close()
  await swarmA.destroy()
  await swarmB.destroy()
  fs.rmSync(srcDir, { recursive: true })
  fs.rmSync(dstDir, { recursive: true })
})

test.skip('DeployClient file-level dedup', async (t) => {
  t.timeout(15000)

  const srcDir = mktemp('dup-src')
  fs.writeFileSync(path.join(srcDir, 'a.txt'), 'unchanged')
  fs.writeFileSync(path.join(srcDir, 'b.txt'), 'will change')

  const dstDir = mktemp('dup-dst')
  fs.writeFileSync(path.join(dstDir, 'a.txt'), 'unchanged')

  const topic = crypto.randomBytes(32)
  const swarmA = new Hyperswarm()
  const swarmB = new Hyperswarm()

  const server = new DeployServer(srcDir, { topic, swarm: swarmA })
  const client = new DeployClient(topic, dstDir, { swarm: swarmB, timeout: 10000 })

  await server.ready()
  await client.ready()

  const result = await client.pull()

  t.is(result.filesTransferred, 1, '1 transferred')
  t.is(result.filesSkipped, 1, '1 skipped')

  await client.close()
  await server.close()
  await swarmA.destroy()
  await swarmB.destroy()
  fs.rmSync(srcDir, { recursive: true })
  fs.rmSync(dstDir, { recursive: true })
})