/**
* 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')
})