// Push ACL gating in the swarm connection handler (issue #37, item 2).
//
// git-upload-pack (read) is always open; git-receive-pack (push) is
// rejected unless the `authorize` hook approves it. Drives the real
// _onConnection logic with a fake conn + stubbed _spawnGitService so no
// swarm or real git process is involved.
const test = require('brittle');
const fs = require('fs');
const os = require('os');
const path = require('path');
const EventEmitter = require('events');
const HyperSync = require('../');
function makeReposDir () {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hs-acl-'));
fs.mkdirSync(path.join(dir, 'r.git'), { recursive: true }); // existsSync(repoPath) → true
return dir;
}
function fakeConn () {
const c = new EventEmitter();
c.destroyed = false;
c.paused = false;
c.remotePublicKey = Buffer.from('ab'.repeat(32), 'hex');
c.destroy = () => { c.destroyed = true; c.emit('close'); };
c.pause = () => { c.paused = true; };
c.resume = () => { c.paused = false; };
c.write = () => true;
return c;
}
// Minimal `this` with just what _onConnection touches — exercises the real
// gating branch without constructing a swarm-backed HyperSync.
function ctx (reposDir, authorize, spawned) {
return {
_reposDir: reposDir,
_topics: new Map([['r', {}]]),
_authorize: authorize,
emit () {},
_spawnGitService (conn, service) { spawned.push(service); }
};
}
function header (service, repo) {
return Buffer.from(service + '\0' + repo + '\0');
}
const tick = () => new Promise((r) => setTimeout(r, 30));
test('upload-pack (read) is always served — no authorize hook needed', async (t) => {
const reposDir = makeReposDir();
const spawned = [];
const conn = fakeConn();
HyperSync.prototype._onConnection.call(ctx(reposDir, null, spawned), conn, {});
conn.emit('data', header('git-upload-pack', 'r'));
await tick();
t.alike(spawned, ['git-upload-pack'], 'upload-pack spawned');
t.absent(conn.destroyed, 'read connection not destroyed');
});
test('receive-pack (push) is rejected when no authorize hook is configured (back-compat)', async (t) => {
const reposDir = makeReposDir();
const spawned = [];
const conn = fakeConn();
HyperSync.prototype._onConnection.call(ctx(reposDir, null, spawned), conn, {});
conn.emit('data', header('git-receive-pack', 'r'));
await tick();
t.alike(spawned, [], 'nothing spawned');
t.ok(conn.destroyed, 'push connection destroyed (read-only default)');
});
test('receive-pack is served when authorize approves', async (t) => {
const reposDir = makeReposDir();
const spawned = [];
const seen = [];
const authorize = async (info) => { seen.push(info); return true; };
const conn = fakeConn();
HyperSync.prototype._onConnection.call(ctx(reposDir, authorize, spawned), conn, {});
conn.emit('data', header('git-receive-pack', 'r'));
await tick();
t.is(seen.length, 1, 'authorize was consulted');
t.is(seen[0].service, 'git-receive-pack', 'authorize got the service');
t.is(seen[0].repo, 'r', 'authorize got the repo');
t.ok(seen[0].remotePublicKey && seen[0].remotePublicKey.equals(conn.remotePublicKey),
'authorize got the peer noise key');
t.alike(spawned, ['git-receive-pack'], 'receive-pack spawned after approval');
t.absent(conn.destroyed, 'approved push not destroyed');
});
test('receive-pack is rejected when authorize denies', async (t) => {
const reposDir = makeReposDir();
const spawned = [];
const authorize = async () => false;
const conn = fakeConn();
HyperSync.prototype._onConnection.call(ctx(reposDir, authorize, spawned), conn, {});
conn.emit('data', header('git-receive-pack', 'r'));
await tick();
t.alike(spawned, [], 'nothing spawned on denial');
t.ok(conn.destroyed, 'denied push destroyed');
});