Files
safetynet/tests/native-patch-preflight.test.mjs
T

51 lines
5.6 KiB
JavaScript
Raw Normal View History

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import {execFile} from 'node:child_process';
import {promisify} from 'node:util';
import {nativePatchPreflight} from '../scripts/native-patch-preflight.mjs';
const exec=promisify(execFile);
async function fixture(t){
const root=await fs.mkdtemp(path.join(os.tmpdir(),'safetynet-native-preflight-'));t.after(()=>fs.rm(root,{recursive:true,force:true}));
const config=path.join(root,'config.json'),patchDir=path.join(root,'patches');await fs.mkdir(patchDir);await fs.writeFile(path.join(patchDir,'.patches'),'one.patch\n');await fs.writeFile(path.join(patchDir,'one.patch'),'synthetic');
await fs.writeFile(config,JSON.stringify([{repo:'src',patch_dir:'patches'}]));
return {root,config,options:{checkoutRoot:root,configPath:config}};
}
async function init(repo){await fs.mkdir(repo,{recursive:true});await exec('git',['init',repo]);await fs.writeFile(path.join(repo,'tracked.txt'),'baseline');await exec('git',['-C',repo,'add','tracked.txt']);await exec('git',['-C',repo,'-c','user.name=Fixture','-c','[email protected]','-c','commit.gpgsign=false','commit','-m','baseline']);}
test('accepts an independent clean checkout and leaves HEAD unchanged',async t=>{
const f=await fixture(t),repo=path.join(f.root,'src');await init(repo);const before=(await exec('git',['-C',repo,'rev-parse','HEAD'])).stdout.trim();const r=await nativePatchPreflight(f.options);assert.equal(r.passed,true);assert.equal(r.required[0].head,before);assert.equal(r.required[0].patchCount,1);assert.equal((await exec('git',['-C',repo,'rev-parse','HEAD'])).stdout.trim(),before);
});
test('rejects existing child directory whose Git resolves to ancestor',async t=>{
const f=await fixture(t);await init(f.root);await fs.mkdir(path.join(f.root,'src'));const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['missing-own-git-marker']);
});
test('missing required target fails even when patch directory exists',async t=>{
const f=await fixture(t);const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.equal(r.requiredCount,1);assert.deepEqual(r.errors[0].errors,['missing-required-path']);
});
test('rejects changed tracked files',async t=>{
const f=await fixture(t);await init(path.join(f.root,'src'));await fs.writeFile(path.join(f.root,'src/tracked.txt'),'changed');const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['tracked-worktree-not-clean']);
});
test('rejects residual git-am state despite clean tracked files',async t=>{
const f=await fixture(t),repo=path.join(f.root,'src');await init(repo);await fs.mkdir(path.join(repo,'.git/rebase-apply'));const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['git-am-in-progress']);assert.equal((await exec('git',['-C',repo,'status','--porcelain=v1','--untracked-files=no'])).stdout.trim(),'');
});
test('rejects checkout symlink or junction escaping supplied root',async t=>{
const f=await fixture(t),outside=await fs.mkdtemp(path.join(os.tmpdir(),'safetynet-preflight-outside-'));t.after(()=>fs.rm(outside,{recursive:true,force:true}));await init(outside);await fs.symlink(outside,path.join(f.root,'src'),process.platform==='win32'?'junction':'dir');const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['path-outside-checkout']);
});
test('rejects escaping config and missing listed patch',async t=>{
const f=await fixture(t);await init(path.join(f.root,'src'));await fs.rm(path.join(f.root,'patches/one.patch'));assert.equal((await nativePatchPreflight(f.options)).passed,false);const r=await nativePatchPreflight({...f.options,configPath:path.join(f.root,'../outside.json')});assert.equal(r.passed,false);
});
test('fontconfig exclusion is opt-in, Windows-only, exact path and DEPS condition checked',async t=>{
const f=await fixture(t);await init(path.join(f.root,'src'));await fs.mkdir(path.join(f.root,'src/third_party/fontconfig/src'),{recursive:true});await fs.writeFile(path.join(f.root,'src/DEPS'),"deps = {'src/third_party/fontconfig/src': {'url':'fixture','condition':'checkout_linux'}}");await fs.writeFile(f.config,JSON.stringify([{repo:'src/third_party/fontconfig/src',patch_dir:'patches'}]));
assert.equal((await nativePatchPreflight(f.options)).passed,false);
const options={...f.options,excludeWindowsFontconfig:true,platform:'win32'};const good=await nativePatchPreflight(options);assert.equal(good.passed,true);assert.equal(good.excludedCount,1);assert.equal(good.requiredCount,0);
assert.equal((await nativePatchPreflight({...options,platform:'linux'})).passed,false);
await fs.writeFile(path.join(f.root,'src/DEPS'),"deps={'src/third_party/fontconfig/src':{'condition':'checkout_win'}}");assert.equal((await nativePatchPreflight(options)).passed,false);
});
test('rejects listed patch traversal outside its patch directory',async t=>{
const f=await fixture(t);await init(path.join(f.root,'src'));await fs.writeFile(path.join(f.root,'other.patch'),'synthetic');await fs.writeFile(path.join(f.root,'patches/.patches'),'../other.patch\n');const r=await nativePatchPreflight(f.options);assert.equal(r.passed,false);assert.deepEqual(r.errors[0].errors,['patch-outside-patch-directory']);
});
test('CLI requires explicit roots and returns failing status',async()=>{
await assert.rejects(exec(process.execPath,['scripts/native-patch-preflight.mjs']),error=>error.code===1&&JSON.parse(error.stdout).passed===false);
});