OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / test / unit / archive-integration.test.js

141 lines · javascript ref:
/**
 * Integration tests for archive generation + extraction (end-to-end)
 *
 * Validates the complete archive flow: generate from a bare git repo,
 * extract with tar, and verify the contents match the release branch.
 */

import test from 'brittle'
import fs from 'bare-fs'
import os from 'bare-os'
import path from 'bare-path'
import subprocess from 'bare-subprocess'
import { generateArchive } from '../../lib/archive.js'
import { parseArchiveFormat } from '../../lib/routes/archive.js'

// ========================================
// Helpers
// ========================================

function tmpDir (label) {
  const dir = path.join(os.tmpdir(), 'archive-integ-' + label + '-' + Date.now() + '-' + Math.random().toString(36).slice(2))
  fs.mkdirSync(dir, { recursive: true })
  return dir
}

function cleanup (dir) {
  try { fs.rmSync(dir, { recursive: true }) } catch {}
}

/**
 * Run a command asynchronously via bare-subprocess.
 * Collects stdout, rejects on non-zero exit.
 */
function exec (cwd, cmd, args) {
  return new Promise((resolve, reject) => {
    const proc = subprocess.spawn(cmd, args, {
      cwd,
      stdio: ['ignore', 'pipe', 'pipe']
    })

    const chunks = []
    const errChunks = []

    proc.stdout.on('data', (c) => chunks.push(c))
    proc.stderr.on('data', (c) => errChunks.push(c))

    proc.on('close', (code) => {
      if (code === 0) {
        resolve(Buffer.concat(chunks).toString('utf-8'))
      } else {
        const stderr = Buffer.concat(errChunks).toString('utf-8')
        reject(new Error(`${cmd} ${args.join(' ')} failed (exit ${code}): ${stderr}`))
      }
    })

    proc.on('error', reject)
  })
}

/**
 * Create a temporary bare git repo with a release branch containing:
 * - package.json (name: test-app, version: 1.0.0)
 * - index.js (on main and release)
 * - built.js (release-only file)
 *
 * Returns the path to the bare repo.
 */
async function createTestRepo (dir) {
  const repoDir = path.join(dir, 'test-repo.git')
  const workDir = path.join(dir, 'work')

  fs.mkdirSync(workDir, { recursive: true })
  await exec(workDir, 'git', ['init'])
  await exec(workDir, 'git', ['config', 'user.email', 'test@test.com'])
  await exec(workDir, 'git', ['config', 'user.name', 'Test'])
  await exec(workDir, 'git', ['checkout', '-b', 'main'])
  fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify({ name: 'test-app', version: '1.0.0', main: 'index.js' }))
  fs.writeFileSync(path.join(workDir, 'index.js'), 'console.log("hello")\n')
  await exec(workDir, 'git', ['add', '-A'])
  await exec(workDir, 'git', ['commit', '-m', 'initial'])
  await exec(workDir, 'git', ['checkout', '-b', 'release'])
  fs.writeFileSync(path.join(workDir, 'built.js'), 'module.exports = true\n')
  await exec(workDir, 'git', ['add', '-A'])
  await exec(workDir, 'git', ['commit', '-m', 'deploy: v1'])
  await exec(dir, 'git', ['clone', '--bare', workDir, repoDir])

  return repoDir
}

// ========================================
// Test 1: Full flow — generate archive then extract matches repo content
// ========================================

test('full flow: generate archive then extract matches repo content', async (t) => {
  const dir = tmpDir('full-flow')
  const archiveDir = path.join(dir, 'archives')
  const extractDir = path.join(dir, 'extracted')
  fs.mkdirSync(extractDir, { recursive: true })

  try {
    // 1. Create a bare git repo with a release branch
    const bareRepoPath = await createTestRepo(dir)

    // 2. Generate a tar.gz archive
    const result = await generateArchive(bareRepoPath, 'test-app', archiveDir, 'tar.gz')

    t.ok(result.path, 'archive path returned')
    t.ok(result.size > 0, 'archive has content')
    t.is(result.format, 'tar.gz', 'format is tar.gz')
    t.ok(typeof result.commitHash === 'string' && result.commitHash.length >= 7, 'has commit hash')

    // 3. Extract with tar xzf --strip-components=1 (same as pgh app install)
    await exec(extractDir, 'tar', ['xzf', result.path, '--strip-components=1'])

    // 4. Verify extracted files exist
    t.ok(fs.existsSync(path.join(extractDir, 'package.json')), 'package.json exists')
    t.ok(fs.existsSync(path.join(extractDir, 'index.js')), 'index.js exists')
    t.ok(fs.existsSync(path.join(extractDir, 'built.js')), 'built.js exists (release-only file)')

    // 5. Verify package.json contents
    const pkg = JSON.parse(fs.readFileSync(path.join(extractDir, 'package.json'), 'utf-8'))
    t.is(pkg.name, 'test-app', 'package.json name is test-app')
    t.is(pkg.version, '1.0.0', 'package.json version is 1.0.0')
  } finally {
    cleanup(dir)
  }
})

// ========================================
// Test 2: parseArchiveFormat handles all expected inputs
// ========================================

test('parseArchiveFormat handles all expected inputs', (t) => {
  t.is(parseArchiveFormat('release.tar.gz'), 'tar.gz', 'release.tar.gz -> tar.gz')
  t.is(parseArchiveFormat('release.zip'), 'zip', 'release.zip -> zip')
  t.is(parseArchiveFormat('release.tar.bz2'), null, 'release.tar.bz2 -> null')
  t.is(parseArchiveFormat('malicious.tar.gz'), null, 'malicious.tar.gz -> null')
  t.is(parseArchiveFormat(null), null, 'null -> null')
  t.is(parseArchiveFormat(undefined), null, 'undefined -> null')
})