127 lines
5.8 KiB
JavaScript
127 lines
5.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { mkdtemp, readFile, rm, writeFile, mkdir } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
import test from 'node:test';
|
|
import { DeploymentError, GitService } from '../src/git-service.mjs';
|
|
|
|
const exec = promisify(execFile);
|
|
|
|
async function git(cwd, ...args) {
|
|
const { stdout } = await exec('git', ['-C', cwd, ...args]);
|
|
return stdout.trim();
|
|
}
|
|
|
|
async function fixture() {
|
|
const root = await mkdtemp(join(tmpdir(), 'image-hook-test-'));
|
|
const remote = join(root, 'remote.git');
|
|
const seed = join(root, 'seed');
|
|
const deployed = join(root, 'deployed');
|
|
await exec('git', ['init', '--bare', remote]);
|
|
await exec('git', ['init', '-b', 'master', seed]);
|
|
await git(seed, 'config', 'user.email', 'test@example.invalid');
|
|
await git(seed, 'config', 'user.name', 'Image Test');
|
|
await mkdir(join(seed, 'icons'), { recursive: true });
|
|
await mkdir(join(seed, 'hook'), { recursive: true });
|
|
await writeFile(join(seed, 'icons', 'first.jpg'), 'first');
|
|
await writeFile(join(seed, 'hook', '.keep'), '');
|
|
await git(seed, 'add', '.');
|
|
await git(seed, 'commit', '-m', 'initial');
|
|
await git(seed, 'remote', 'add', 'origin', remote);
|
|
await git(seed, 'push', '-u', 'origin', 'master');
|
|
await exec('git', ['clone', remote, deployed]);
|
|
|
|
const config = {
|
|
repositoryPath: deployed,
|
|
remoteUrl: remote,
|
|
repositoryFullName: 'devsam/image',
|
|
defaultBranch: 'master',
|
|
allowedBranches: ['master', 'preview'],
|
|
statePath: join(root, 'state', 'state.json'),
|
|
publicBases: ['https://sam.hided.net/image', 'https://sam-image.hided.net'],
|
|
};
|
|
const service = new GitService(config, { now: () => new Date('2026-08-06T00:00:00Z') });
|
|
await service.initialize();
|
|
return { root, remote, seed, deployed, service };
|
|
}
|
|
|
|
test('push deployment fast-forwards, writes inventory, and deduplicates delivery IDs', async (t) => {
|
|
const f = await fixture();
|
|
t.after(() => rm(f.root, { recursive: true, force: true }));
|
|
await writeFile(join(f.seed, 'icons', '둘째.png'), 'second');
|
|
await git(f.seed, 'add', '.');
|
|
await git(f.seed, 'commit', '-m', 'second');
|
|
await git(f.seed, 'push', 'origin', 'master');
|
|
const target = await git(f.seed, 'rev-parse', 'HEAD');
|
|
|
|
const result = await f.service.deployWebhook({ deliveryId: 'delivery-0001', branch: 'master', after: target });
|
|
assert.equal(result.changed, true);
|
|
assert.equal(await git(f.deployed, 'rev-parse', 'HEAD'), target);
|
|
const inventory = JSON.parse(await readFile(join(f.deployed, 'hook', 'inventory.v2.json'), 'utf8'));
|
|
assert.equal(inventory.directories['.']['둘째'], '둘째.png');
|
|
assert.equal((await f.service.deployWebhook({ deliveryId: 'delivery-0001', branch: 'master', after: target })).duplicate, true);
|
|
});
|
|
|
|
test('inactive branches are ignored and tracked changes block deployment', async (t) => {
|
|
const f = await fixture();
|
|
t.after(() => rm(f.root, { recursive: true, force: true }));
|
|
assert.equal((await f.service.deployWebhook({ deliveryId: 'delivery-0002', branch: 'preview', after: 'a'.repeat(40) })).ignored, true);
|
|
await writeFile(join(f.deployed, 'icons', 'first.jpg'), 'dirty');
|
|
await assert.rejects(
|
|
f.service.deployWebhook({ deliveryId: 'delivery-0003', branch: 'master', after: await git(f.seed, 'rev-parse', 'HEAD') }),
|
|
(error) => error instanceof DeploymentError && /Tracked worktree/.test(error.message),
|
|
);
|
|
});
|
|
|
|
test('admin deploy explicitly switches to an allowlisted unrelated branch', async (t) => {
|
|
const f = await fixture();
|
|
t.after(() => rm(f.root, { recursive: true, force: true }));
|
|
await git(f.seed, 'checkout', '--orphan', 'preview');
|
|
await git(f.seed, 'rm', '-rf', '.');
|
|
await mkdir(join(f.seed, 'icons'), { recursive: true });
|
|
await mkdir(join(f.seed, 'hook'), { recursive: true });
|
|
await writeFile(join(f.seed, 'icons', 'preview.jpg'), 'preview');
|
|
await writeFile(join(f.seed, 'hook', '.keep'), '');
|
|
await git(f.seed, 'add', '.');
|
|
await git(f.seed, 'commit', '-m', 'preview');
|
|
await git(f.seed, 'push', 'origin', 'preview');
|
|
const target = await git(f.seed, 'rev-parse', 'HEAD');
|
|
|
|
const result = await f.service.deployAdmin({ requestId: 'admin-request-1', branch: 'preview', expectedCommit: target });
|
|
assert.equal(result.changed, true);
|
|
assert.equal(f.service.publicStatus().activeBranch, 'preview');
|
|
await assert.rejects(
|
|
f.service.deployAdmin({ requestId: 'admin-request-2', branch: 'forbidden' }),
|
|
/Branch is not allowed/,
|
|
);
|
|
});
|
|
|
|
test('same-branch force pushes and payload SHA mismatches are rejected', async (t) => {
|
|
const f = await fixture();
|
|
t.after(() => rm(f.root, { recursive: true, force: true }));
|
|
const initial = await git(f.seed, 'rev-parse', 'HEAD');
|
|
await writeFile(join(f.seed, 'icons', 'second.jpg'), 'second');
|
|
await git(f.seed, 'add', '.');
|
|
await git(f.seed, 'commit', '-m', 'second');
|
|
await git(f.seed, 'push', 'origin', 'master');
|
|
const second = await git(f.seed, 'rev-parse', 'HEAD');
|
|
await f.service.deployWebhook({ deliveryId: 'delivery-0004', branch: 'master', after: second });
|
|
|
|
await git(f.seed, 'reset', '--hard', initial);
|
|
await writeFile(join(f.seed, 'icons', 'forced.jpg'), 'forced');
|
|
await git(f.seed, 'add', '.');
|
|
await git(f.seed, 'commit', '-m', 'forced replacement');
|
|
await git(f.seed, 'push', '--force', 'origin', 'master');
|
|
const forced = await git(f.seed, 'rev-parse', 'HEAD');
|
|
await assert.rejects(
|
|
f.service.deployWebhook({ deliveryId: 'delivery-0005', branch: 'master', after: forced }),
|
|
/Non-fast-forward deployment rejected/,
|
|
);
|
|
await assert.rejects(
|
|
f.service.deployWebhook({ deliveryId: 'delivery-0006', branch: 'master', after: 'f'.repeat(40) }),
|
|
/Payload commit does not match remote branch tip/,
|
|
);
|
|
});
|