mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 08:17:26 +01:00
104 lines
7.8 KiB
JavaScript
104 lines
7.8 KiB
JavaScript
'use strict';
|
|
// Seed and check synthetic data only after proving this is the isolated stack.
|
|
const assert = require('node:assert/strict');
|
|
const {spawnSync} = require('node:child_process');
|
|
const path = require('node:path');
|
|
const root = path.resolve(__dirname, '../..');
|
|
const app = process.argv.includes('--app');
|
|
const beta = app || process.argv.includes('--beta');
|
|
const compose = ['compose', '--env-file', '.env.go-shadow', '-f', 'backend/compose.shadow.yml'];
|
|
if (beta) compose.push('-f', 'backend/compose.shadow-beta.yml');
|
|
if (app) compose.push('-f', 'backend/compose.shadow-app.yml');
|
|
function docker(args, input) {
|
|
const result = spawnSync('docker', args, {cwd: root, input, encoding: 'utf8', timeout: 30000});
|
|
if (result.error || result.status !== 0) throw Error('Shadow Docker operation failed');
|
|
return result.stdout.trim();
|
|
}
|
|
(async () => {
|
|
const cid = docker([...compose, 'ps', '-q', 'app']);
|
|
assert(cid, 'shadow app must be running');
|
|
const info = JSON.parse(docker(['inspect', cid]))[0];
|
|
assert.equal(info.Config.Labels['com.docker.compose.project'], 'anonymous-go-shadow');
|
|
assert.equal(info.Config.User, '10001:10001');
|
|
assert.equal(info.HostConfig.ReadonlyRootfs, true);
|
|
assert(Object.values(info.NetworkSettings.Ports || {}).every(p => !p));
|
|
const gateway = JSON.parse(docker(['inspect', docker([...compose,'ps','-q','gateway'])]))[0];
|
|
assert.equal(gateway.Config.Labels['com.docker.compose.project'], 'anonymous-go-shadow');
|
|
assert.deepEqual(gateway.NetworkSettings.Ports['8080/tcp'], [{HostIp:'127.0.0.1',HostPort:'5001'}]);
|
|
for (const service of ['mongodb','redis']) {
|
|
const database = JSON.parse(docker(['inspect',docker([...compose,'ps','-q',service])]))[0];
|
|
assert.equal(database.Config.Labels['com.docker.compose.project'], 'anonymous-go-shadow');
|
|
assert.deepEqual(Object.keys(database.NetworkSettings.Networks), ['anonymous-go-shadow_default']);
|
|
assert(Object.values(database.NetworkSettings.Ports || {}).every(p => !p));
|
|
assert(database.Mounts.every(m => m.Type==='volume' && m.Name.startsWith('anonymous-go-shadow_')));
|
|
}
|
|
const networks = Object.keys(info.NetworkSettings.Networks);
|
|
assert.deepEqual(networks.sort(), beta ? ['anonymous-go-shadow_default','anonymous-go-shadow_outbound'] : ['anonymous-go-shadow_default']);
|
|
assert.equal(JSON.parse(docker(['network', 'inspect', networks[0]]))[0].Internal, true);
|
|
assert(info.Mounts.every(m => (m.Type === 'volume' && m.Name.startsWith('anonymous-go-shadow_')) || (app && m.Type === 'bind' && m.Source === path.join(root,'secrets/github-app-beta.pem') && m.Destination === '/run/secrets/github-app-beta.pem' && !m.RW)));
|
|
const env = Object.fromEntries(info.Config.Env.map(x => {const i=x.indexOf('=');return [x.slice(0,i),x.slice(i+1)];}));
|
|
const mongo = new URL(env.MONGODB_URI);
|
|
assert.equal(mongo.hostname, 'mongodb');
|
|
assert.equal(mongo.pathname, '/shadow');
|
|
assert.equal(env.REDIS_HOSTNAME, 'redis');
|
|
assert.equal(env.GITHUB_APP_ENABLED, app ? 'true' : 'false');
|
|
assert.equal(env.GITHUB_OAUTH_ENABLED, beta ? 'true' : 'false');
|
|
if(beta) {
|
|
assert(env.CLIENT_ID && env.CLIENT_SECRET);
|
|
assert.equal(env.AUTH_CALLBACK,'https://beta.durieux.me/github/auth');
|
|
assert.equal(env.COOKIE_SECURE,'true');
|
|
assert.equal(env.TRUST_PROXY,'2');
|
|
} else assert(!env.CLIENT_SECRET);
|
|
for (const key of ['GITHUB_TOKEN','GITHUB_APP_PRIVATE_KEY','S3_CLIENT_SECRET',...(!app?['GITHUB_APP_CLIENT_SECRET']:[])]) assert(!env[key], 'no unexpected upstream credentials');
|
|
if(app) {assert(env.GITHUB_APP_CLIENT_SECRET);assert.equal(env.GITHUB_APP_PRIVATE_KEY_FILE,'/run/secrets/github-app-beta.pem');assert.equal(env.GITHUB_APP_CALLBACK,'https://beta.durieux.me/github/app/callback');}
|
|
const body = '# Alice shadow demo\n\nhello Alice\n';
|
|
docker([...compose, 'exec', '-T', 'app', 'sh', '-ec', 'mkdir -p /app/repositories/shadow-demo/original; cat > /app/repositories/shadow-demo/original/README.md'], body);
|
|
const script = `
|
|
const connection = new Mongo('mongodb://' + encodeURIComponent(process.env.MONGO_INITDB_ROOT_USERNAME) + ':' + encodeURIComponent(process.env.MONGO_INITDB_ROOT_PASSWORD) + '@127.0.0.1:27017/?authSource=admin');
|
|
const shadow = connection.getDB('shadow');
|
|
const owner = ObjectId('000000000000000000000001');
|
|
const existing = shadow.anonymizedrepositories.findOne({repoId:'shadow-demo'});
|
|
if (existing && existing.owner.toString() !== owner.toString()) throw Error('unexpected fixture owner');
|
|
shadow.users.updateOne({_id:owner},{$setOnInsert:{username:'shadow-fixture-owner',status:'active',isAdmin:false}},{upsert:true});
|
|
shadow.anonymizedrepositories.updateOne({repoId:'shadow-demo'},{$setOnInsert:{owner,status:'ready',statusDate:new Date(),anonymizeDate:new Date('2026-01-01'),settingsSavedAt:new Date('2026-01-01'),treeGeneration:'shadow-fixture-v1',pageView:0,source:{type:'Zip',repositoryName:'fixture/project',branch:'main',commit:'fixture-v1'},options:{terms:['Alice'],expirationMode:'never',image:true,pdf:true,link:true,update:false}}},{upsert:true});
|
|
shadow.files.updateOne({repoId:'shadow-demo',path:'',name:'README.md'},{$setOnInsert:{sha:'fixture-readme',size:${Buffer.byteLength(body)},treeGeneration:'shadow-fixture-v1'}},{upsert:true});
|
|
`;
|
|
docker([...compose,'exec','-T','mongodb','mongosh','--quiet','--nodb'], script);
|
|
const base = beta ? 'https://beta.durieux.me' : 'http://127.0.0.1:5001';
|
|
for (const route of ['/api/healthcheck','/','/r/shadow-demo/']) {
|
|
const response = await fetch(base+route,{headers:{Accept:'text/html'},signal:AbortSignal.timeout(10000)});
|
|
assert.equal(response.status,200,route);await response.arrayBuffer();
|
|
}
|
|
const options = await (await fetch(base+'/api/options')).json();
|
|
assert.equal(options.GITHUB_APP_ENABLED,app);assert.equal(options.GITHUB_OAUTH_ENABLED,beta);
|
|
if(beta) {
|
|
const login = await fetch(base+'/github/login',{redirect:'manual'});
|
|
assert.equal(login.status,302);
|
|
const target=new URL(login.headers.get('location'));
|
|
assert.equal(target.origin,'https://github.com');
|
|
assert.equal(target.pathname,'/login/oauth/authorize');
|
|
assert.equal(target.searchParams.get('client_id'),env.CLIENT_ID);
|
|
assert.equal(target.searchParams.get('redirect_uri'),env.AUTH_CALLBACK);
|
|
assert.match(login.headers.get('set-cookie'),/;\s*Secure/i);
|
|
assert.match(login.headers.get('set-cookie'),/;\s*HttpOnly/i);
|
|
assert.match(login.headers.get('set-cookie'),/;\s*SameSite=Lax/i);
|
|
}
|
|
if(app) {
|
|
const login=await fetch(base+'/github/app/login',{redirect:'manual'});
|
|
assert.equal(login.status,302);
|
|
const target=new URL(login.headers.get('location'));
|
|
assert.equal(target.origin,'https://github.com');
|
|
assert.equal(target.searchParams.get('client_id'),env.GITHUB_APP_CLIENT_ID);
|
|
assert.equal(target.searchParams.get('redirect_uri'),env.GITHUB_APP_CALLBACK);
|
|
assert(!target.searchParams.has('scope'));
|
|
}
|
|
assert.equal((await fetch(base+'/api/user')).status,401);
|
|
const expected = body.replaceAll('Alice','XXXX-1');
|
|
const file = base+'/api/repo/shadow-demo/file/README.md';
|
|
const cold = await fetch(file);assert.equal(cold.status,200);assert.equal(await cold.text(),expected);
|
|
await Promise.all(Array.from({length:8},async()=>{const r=await fetch(file);assert.equal(r.status,200);assert.equal(await r.text(),expected);}));
|
|
const zip=await fetch(base+'/api/repo/shadow-demo/zip');assert.equal(zip.status,200);
|
|
const bytes=Buffer.from(await zip.arrayBuffer());assert.equal(bytes.readUInt32LE(0),0x04034b50);
|
|
console.log(JSON.stringify({isolation:'passed',health:'passed',authentication:beta?(app?'App and legacy OAuth redirects verified; App authorization pending':'legacy OAuth redirect and secure cookie verified; user sign-in still required'):'disabled until a separate callback is configured',exactAnonymizedFileChecks:9,zip:'passed',demo:base+'/r/shadow-demo/'},null,2));
|
|
})().catch(error => {console.error(error.message);process.exitCode=1;});
|