mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 08:17:26 +01:00
378 lines
25 KiB
JavaScript
378 lines
25 KiB
JavaScript
'use strict';
|
|
// Explicitly authorized beta-only admin bootstrap and authenticated deployment checks.
|
|
// Credentials remain in memory. The temporary key is removed even when a check fails.
|
|
const assert = require('node:assert/strict');
|
|
const {randomBytes, createHash} = require('node:crypto');
|
|
const {spawnSync} = require('node:child_process');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const root = path.resolve(__dirname, '../..');
|
|
const base = 'https://beta.durieux.me';
|
|
const compose = ['compose', '--env-file', '.env.go-shadow', '-f', 'backend/compose.shadow.yml', '-f', 'backend/compose.shadow-beta.yml'];
|
|
const appFlow = process.argv.includes('--app-flow');
|
|
const monitoringOnly = process.argv.includes('--monitoring-only');
|
|
const app = appFlow || process.argv.includes('--app');
|
|
if(app) compose.push('-f','backend/compose.shadow-app.yml');
|
|
function docker(args, input) {
|
|
const r = spawnSync('docker', args, {cwd:root, input, encoding:'utf8', timeout:30000});
|
|
if (r.error || r.status !== 0) throw Error('Shadow Docker operation failed');
|
|
return r.stdout.trim();
|
|
}
|
|
function db(script) {
|
|
return docker([...compose, 'exec', '-T', 'mongodb', 'mongosh', '--quiet', '--nodb', '--file', '/dev/stdin'], `
|
|
const c = 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 d = c.getDB('shadow');
|
|
${script}
|
|
`);
|
|
}
|
|
const report = {verifiedAt:new Date().toISOString(), hostname:'beta.durieux.me', configurationMode:app?'GitHub App enabled alongside legacy OAuth':'legacy OAuth only', checks:[], productionChanged:false};
|
|
const key = randomBytes(32).toString('hex');
|
|
const hash = createHash('sha256').update(key).digest('hex');
|
|
const id = randomBytes(12).toString('hex');
|
|
const repoId = 'go-beta-check-' + Date.now();
|
|
let bootstrapped = false, createdRepo = false;
|
|
const requestIDs=new Set();
|
|
const fixture = appFlow ? {name:'tdurieux/synctex-js',branch:'master',file:'LICENSE',term:'MIT'} : {name:'octocat/Hello-World',branch:'master',file:'README',term:'Hello'};
|
|
async function call(method, route, body, expected=200, token=key, extraHeaders={}, binary=false) {
|
|
const start = performance.now();
|
|
const response = await fetch(base+route, {method, headers:{...(token ? {Authorization:'Bearer '+token}:{}), 'Content-Type':'application/json', ...extraHeaders}, body:body===undefined?undefined:JSON.stringify(body), signal:AbortSignal.timeout(45000), redirect:'manual'});
|
|
assert.equal(response.status, expected, method+' '+route+' unexpected status; deployment='+response.headers.get('x-anonymous-deployment'));
|
|
const requestID=response.headers.get('x-request-id');
|
|
assert(/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(requestID||''),'missing generated request ID on '+method+' '+route+' (status '+response.status+', deployment '+response.headers.get('x-anonymous-deployment')+')');
|
|
assert(!requestIDs.has(requestID),'request ID reused');
|
|
requestIDs.add(requestID);
|
|
const headersMs=Math.round(performance.now()-start);
|
|
const chunks=[];
|
|
let firstBodyByteMs=null;
|
|
for await (const chunk of response.body) {
|
|
if(chunk.length && firstBodyByteMs===null) firstBodyByteMs=Math.round(performance.now()-start);
|
|
chunks.push(chunk);
|
|
}
|
|
const bytes = Buffer.concat(chunks);
|
|
const text = bytes.toString('utf8');
|
|
let value; try {value=JSON.parse(text);} catch {value=text;}
|
|
report.checks.push({method, route, requestID, status:response.status, expected, bytes:bytes.length, headersMs, firstBodyByteMs, elapsedMs:Math.round(performance.now()-start)});
|
|
assert.equal(response.headers.get('x-anonymous-deployment'), 'go-shadow-beta', 'wrong deployment');
|
|
assert.equal(response.status, expected, method+' '+route+' unexpected status');
|
|
assert(!text.includes(key) && !text.includes(hash), 'credential exposure');
|
|
return {value:binary?bytes:value,response};
|
|
}
|
|
(async () => {
|
|
assert(process.argv.includes('--bootstrap-admin'), 'Explicit --bootstrap-admin required; leaves the sole real beta account as admin');
|
|
for (const service of ['app','mongodb']) {
|
|
const info=JSON.parse(docker(['inspect',docker([...compose,'ps','-q',service])]))[0];
|
|
assert.equal(info.Config.Labels['com.docker.compose.project'],'anonymous-go-shadow');
|
|
assert(Object.values(info.NetworkSettings.Ports||{}).every(v=>!v));
|
|
assert(info.Mounts.every(m=>(m.Type==='volume' && m.Name.startsWith('anonymous-go-shadow_')) || (app && service==='app' && m.Type==='bind' && m.Source===path.join(root,'secrets/github-app-beta.pem') && m.Destination==='/run/secrets/github-app-beta.pem' && !m.RW)));
|
|
if(service==='mongodb') assert.deepEqual(Object.keys(info.NetworkSettings.Networks),['anonymous-go-shadow_default']);
|
|
if(service==='app') {
|
|
const env=Object.fromEntries(info.Config.Env.map(v=>{const i=v.indexOf('=');return [v.slice(0,i),v.slice(i+1)];}));
|
|
const uri=new URL(env.MONGODB_URI);
|
|
assert.equal(uri.hostname,'mongodb');assert.equal(uri.pathname,'/shadow');
|
|
assert.equal(env.APP_HOSTNAME,'beta.durieux.me');
|
|
}
|
|
}
|
|
await call('GET','/api/user',undefined,401,'');
|
|
db(`const users=d.users.find({'externalIDs.github':{$exists:true},status:'active'}).toArray();
|
|
if(users.length!==1) throw Error('Expected exactly one active GitHub beta account');
|
|
const result=d.users.updateOne({_id:users[0]._id,status:'active'},{$set:{isAdmin:true},$push:{apiTokens:{_id:ObjectId('${id}'),name:'Temporary beta deployment validation',tokenHash:'${hash}',createdAt:new Date()}}});
|
|
if(result.modifiedCount!==1) throw Error('Bootstrap failed');`);
|
|
bootstrapped=true;
|
|
const {value:user}=await call('GET','/api/user');
|
|
assert.equal(user.isAdmin,true);
|
|
for(const route of (monitoringOnly ? ['/api/admin/overview','/api/admin/queues','/api/admin/performance','/api/admin/errors','/api/admin/errors/stats'] : ['/github/connections','/api/providers','/api/user/dashboard','/api/user/default','/api/user/quota','/api/admin/overview','/api/admin/users','/api/admin/repos','/api/admin/queues','/api/admin/performance','/api/admin/errors','/api/admin/errors/stats','/api/admin/tokens'])) {
|
|
const {value}=await call('GET',route);
|
|
if(route==='/api/admin/queues') {
|
|
for(const queue of value.queues) {
|
|
for(const field of ['workers','completed24h','failed24h']) assert(Number.isFinite(queue[field]),'missing queue overview '+field);
|
|
assert(queue.workers>=1,'beta worker not registered: '+queue.key);
|
|
assert.equal(queue.concurrency,queue.key==='download'?3:5,'wrong beta worker concurrency');
|
|
}
|
|
report.queueOverviewFields='passed';
|
|
}
|
|
if(route==='/api/admin/overview') {
|
|
for(const [group,field] of [['repos','activeRepos24h'],['repos','newRepos24h'],['users','newUsers24h'],['system','cpuPercent'],['system','memPercent'],['system','diskPercent']]) {
|
|
assert(Number.isFinite(value[group]?.[field]),'Missing overview field '+group+'.'+field);
|
|
}
|
|
const today=new Date();today.setUTCHours(0,0,0,0);
|
|
const current=value.history.filter(row=>new Date(row.date).getTime()===today.getTime());
|
|
assert.equal(current.length,1,'admin history should contain one current-day snapshot');
|
|
assert.equal(current[0].nbRepositories,value.repos.total,'admin history uses stale totals');
|
|
report.overviewHistoryFresh='passed';
|
|
report.overviewBrowserFields='passed';
|
|
}
|
|
}
|
|
const {value:errorList}=await call('GET','/api/admin/errors?limit=5001');
|
|
assert.equal(errorList.max,5000);assert.equal(errorList.limit,5000);assert.equal(errorList.available,true);
|
|
const {value:beforeErrors}=await call('GET','/api/admin/errors/stats');
|
|
await call('GET','/api/repo/'+repoId+'-missing',undefined,404);
|
|
const {value:afterErrors}=await call('GET','/api/admin/errors/stats');
|
|
assert.equal(afterErrors.available,true);assert.equal(afterErrors.buckets.length,24);
|
|
assert(afterErrors.severity.info>=beforeErrors.severity.info+1,'missing routine 404 counter');
|
|
report.errorMonitoring={maxEntries:5000,hourlyBuckets:24,routine404Counted:true};
|
|
const windows=[];
|
|
for(const [queue,range,count] of [['download','1h',60],['download','6h',360],['download','24h',1440],['download','7d',10080],['remove','1h',60],['cache','1h',60],['download','unrecognized',60]]) {
|
|
const {value:history}=await call('GET','/api/admin/queues/metrics?queue='+queue+'&range='+range);
|
|
assert.equal(history.queue,queue);assert.equal(history.range,range);assert.equal(history.points.length,count);
|
|
assert.equal(history.points.at(-1).ts-history.points[0].ts,(count-1)*60000);
|
|
for(const point of history.points) for(const field of ['ts','completed','failed','avgMs']) assert(Number.isFinite(point[field]),'invalid queue point '+field);
|
|
windows.push({queue,range,points:count});
|
|
}
|
|
await call('GET','/api/admin/queues/metrics?queue=unknown',undefined,404);
|
|
report.queueMetricWindows=windows;
|
|
const statFields=['nbRepositories','nbUsers','nbPageViews','nbPullRequests'];
|
|
const {value:publicStats}=await call('GET','/api/stat',undefined,200,'');
|
|
for(const field of statFields) assert(Number.isFinite(publicStats[field]) && publicStats[field]>=0,'invalid public statistic '+field);
|
|
const publicWindows=[];
|
|
for(const [query,days] of [['days=-2',1],['days=0',30],['days=0x10',16],['days=7suffix',7],['days=999',365],['days[]=7&days[]=30',7],['days[1]=7&days[0]=1',1],['days[x]=7',30]]) {
|
|
const started=new Date();started.setUTCHours(0,0,0,0);
|
|
const {value:history}=await call('GET','/api/stat/history?'+query,undefined,200,'');
|
|
assert(Array.isArray(history) && history.length>0,'missing public history');
|
|
const lower=started.getTime()-(days-1)*86400000;
|
|
let previous=-Infinity;
|
|
for(const row of history) {
|
|
const date=Date.parse(row.date);
|
|
assert(Number.isFinite(date) && date>=lower && date>=previous,'invalid history window/order');
|
|
previous=date;
|
|
for(const field of statFields) assert(Number.isFinite(row[field]) && row[field]>=0,'invalid history statistic '+field);
|
|
}
|
|
publicWindows.push({query,days,rows:history.length});
|
|
}
|
|
report.publicStatistics={anonymousAccess:true,windows:publicWindows};
|
|
for(const query of ['statuses[]=','type[]=repo']) {
|
|
const {value:dashboard}=await call('GET','/api/user/dashboard?'+query);
|
|
assert.deepEqual(dashboard.items,[],'array-valued dashboard filter not preserved');
|
|
assert.equal(dashboard.filtered,0);
|
|
}
|
|
for(const query of ['sort[]=','cursor[]=']) await call('GET','/api/user/dashboard?'+query,undefined,400);
|
|
report.dashboardExtendedQueries='passed';
|
|
const dateCursor=value=>Buffer.from(JSON.stringify({sort:'anonymizeDate',type:'repo',id:'000000000000000000000001',value})).toString('base64url');
|
|
const cursorRoute=value=>'/api/user/dashboard?sort=anonymizeDate&limit=5&cursor='+dateCursor(value);
|
|
const {value:canonicalDates}=await call('GET',cursorRoute('2024-03-01T00:00:00.000Z'));
|
|
for(const date of ['March 1 2024','2024-02-30','2024-02-29T24:00:00Z',['2024','3','1']]) {
|
|
const {value}=await call('GET',cursorRoute(date));
|
|
assert.deepEqual(value.items,canonicalDates.items);assert.equal(value.cursor,canonicalDates.cursor);
|
|
}
|
|
await call('GET',cursorRoute('not a date'),undefined,400);
|
|
report.dashboardDateCursors='passed';
|
|
for(const [query,expected] of [['minutes=6e1',60],['minutes[]=60',60],['minutes[0]=15',15]]) {
|
|
const {value:performance}=await call('GET','/api/admin/performance?'+query);
|
|
assert.equal(performance.windowMinutes,expected,'monitoring query window mismatch');
|
|
}
|
|
report.monitoringExtendedQueries='passed';
|
|
for(const [query,queue,range,count] of [['queue[]=cache&range[]=6h','cache','6h',360],['range[]=','download','',60],['range=toString','download','toString',0]]) {
|
|
const {value:history}=await call('GET','/api/admin/queues/metrics?'+query);
|
|
assert.equal(history.queue,queue);assert.equal(history.range,range);assert.equal(history.points.length,count);
|
|
}
|
|
await call('GET','/api/admin/queues/metrics?queue[]=',undefined,404);
|
|
const {value:arrayQueue}=await call('GET','/api/admin/queues?queue[]=remove');
|
|
assert.equal(arrayQueue.selectedQueue,'remove');
|
|
const {value:arrayErrors}=await call('GET','/api/admin/errors?offset[]=2&limit[]=7');
|
|
assert.equal(arrayErrors.offset,2);assert.equal(arrayErrors.limit,7);assert(arrayErrors.entries.length<=7);
|
|
report.adminExtendedQueries='passed';
|
|
for(const query of ['connection=','connection[]=oauth','connection=oauth&connection[]=github-app']) {
|
|
const {value:rejected}=await call('GET','/api/repo/octocat/Hello-World?'+query,undefined,400);
|
|
assert.equal(rejected.error,'invalid_connection');
|
|
}
|
|
report.previewConnectionQueries='passed';
|
|
for(const query of ['repositoryID[]=invalid','repositoryID=invalid','repositoryID[$exists]=true']) {
|
|
const {value:rejected}=await call('GET','/api/repo/octocat/Hello-World?'+query,undefined,500);
|
|
assert.equal(rejected.error,'internal_error');
|
|
}
|
|
report.previewCacheHintRejections='passed';
|
|
for(const query of ['q[]=ab','q[length]=1']) {
|
|
const {value}=await call('GET','/api/user/search/github-users?'+query);
|
|
assert.deepEqual(value,[]);
|
|
}
|
|
for(const query of ['q=ab&q=cd','q[x]=ab','q[length][toString]=1']) {
|
|
const {value}=await call('GET','/api/user/search/github-users?'+query,undefined,500);
|
|
assert.equal(value.error,'internal_error');
|
|
}
|
|
report.userSearchQueries='passed';
|
|
for(const kind of ['repos','users','conferences']) {
|
|
const {value}=await call('GET','/api/admin/'+kind+'?page[]=2&limit[]=2&sort[]=status&direction[]=asc&format[]=csv');
|
|
assert.equal(value.page,2);assert.deepEqual(value.sort,{_id:-1});
|
|
const {value:large}=await call('GET','/api/admin/'+kind+'?page=1000001&limit=1');
|
|
assert.equal(large.page,1000001);
|
|
for(const query of ['page=-1','page[toString]=x',...(kind==='users'?['limit=-1']:[])]) {
|
|
const {value:rejected}=await call('GET','/api/admin/'+kind+'?'+query,undefined,500);
|
|
assert.equal(rejected.error,'internal_error');
|
|
}
|
|
}
|
|
report.adminListPaginationQueries='passed';
|
|
for(const kind of ['repos','conferences']) {
|
|
const {value}=await call('GET','/api/admin/'+kind+'?limit=-1');
|
|
assert(value.results.length<=1);
|
|
const {value:flags}=await call('GET','/api/admin/'+kind+'?ready[]=true');
|
|
assert.deepEqual(flags.query,{$and:[{$or:[{status:'ready'}]}]});
|
|
}
|
|
for(const kind of ['repos','users','conferences']) {
|
|
const {value}=await call('GET','/api/admin/'+kind+'?search[]=x',undefined,500);
|
|
assert.equal(value.error,'internal_error');
|
|
}
|
|
report.adminListFilterQueries='passed';
|
|
const {value:arrayStatus}=await call('GET','/api/admin/users?status[]=active');
|
|
assert.deepEqual(arrayStatus.query.status,['active']);
|
|
assert.deepEqual(arrayStatus.results,[]);assert(arrayStatus.total>=1);
|
|
const {value:operatorStatus}=await call('GET','/api/admin/users?status[$in][]=active');
|
|
assert.deepEqual(operatorStatus.query.status,{$in:['active']});
|
|
assert(operatorStatus.results.length>=1);
|
|
for(const route of ['/api/admin/users?status[a]=active','/api/admin/users?status[$in]=active','/api/admin/repos?owner[a]=x','/api/admin/repos?conference[$unknown]=x']) {
|
|
const {value}=await call('GET',route,undefined,500);assert.equal(value.error,'internal_error');
|
|
}
|
|
report.adminListCastingQueries='passed';
|
|
for(const [kind,field] of [['repos','anonymizeDate'],['users','dateOfEntry'],['conferences','startDate']]) {
|
|
const {value}=await call('GET','/api/admin/'+kind+'?dateFrom[]=2024-02-30');
|
|
const filter=kind==='users'?value.query:value.query.$and[0];
|
|
assert.deepEqual(filter[field],{$gte:'2024-03-01T00:00:00.000Z'});
|
|
const {value:legacy}=await call('GET','/api/admin/'+kind+'?dateTo=March%201%202024');
|
|
const legacyFilter=kind==='users'?legacy.query:legacy.query.$and[0];
|
|
assert.deepEqual(legacyFilter[field],{$lte:'2024-03-01T00:00:00.000Z'});
|
|
const {value:invalid}=await call('GET','/api/admin/'+kind+'?dateFrom[toString]=x',undefined,500);
|
|
assert.equal(invalid.error,'internal_error');
|
|
}
|
|
report.adminListDateQueries='passed';
|
|
if(monitoringOnly) {report.result='passed';return;}
|
|
|
|
if(appFlow) {
|
|
const {value:connections}=await call('GET','/github/connections');
|
|
assert(connections.appConnected && !connections.appError && connections.installations.length>0,'App authorization or installation unavailable');
|
|
}
|
|
const {value:issued}=await call('POST','/api/admin/tokens',{name:'Temporary API lifecycle check'});
|
|
assert(typeof issued.id==='string' && /^[a-f0-9]{24}$/.test(issued.id), 'invalid issued key ID');
|
|
try {
|
|
assert(typeof issued.token==='string' && issued.token.length===64,'invalid issued key');
|
|
await call('GET','/api/user',undefined,200,issued.token);
|
|
} finally {
|
|
await call('DELETE','/api/admin/tokens/'+issued.id);
|
|
}
|
|
await call('GET','/api/user',undefined,401,issued.token);
|
|
report.apiKeyLifecycle='passed';
|
|
const {response:login}=await call('POST','/github/login-token',{});
|
|
const setCookie=login.headers.get('set-cookie')||'';
|
|
const cookie=setCookie.split(';')[0];
|
|
assert(cookie.startsWith('connect.sid='),'session cookie missing');
|
|
try {
|
|
assert(/;\s*Secure/i.test(setCookie),'session cookie must be Secure');
|
|
assert(/;\s*HttpOnly/i.test(setCookie),'session cookie must be HttpOnly');
|
|
assert(/;\s*Max-Age=2592000(?:;|$)/i.test(setCookie),'session cookie must persist for 30 days');
|
|
assert(/;\s*Expires=/i.test(setCookie),'persistent session cookie must have an expiry');
|
|
assert(/;\s*SameSite=Lax/i.test(setCookie),'session cookie must use SameSite=Lax');
|
|
const {value:sessionUser}=await call('GET','/api/user',undefined,200,'',{Cookie:cookie});
|
|
assert(sessionUser.isAdmin===true && sessionUser.username===user.username,'session identity mismatch');
|
|
} finally {
|
|
await call('GET','/api/user/logout',undefined,302,'',{Cookie:cookie});
|
|
}
|
|
await call('GET','/api/user',undefined,401,'',{Cookie:cookie});
|
|
report.sessionLoginLogout='passed';
|
|
const {value:preview}=await call('POST','/api/anonymize-preview',{content:'hello Alice',options:{terms:['Alice']}});
|
|
assert.equal(preview.content,'hello XXXX-1');
|
|
const prefix='/api/providers/github/repository/';
|
|
const query='?name='+encodeURIComponent(fixture.name)+'&connection='+(appFlow?'github-app':'oauth');
|
|
await call('GET',prefix+'metadata'+query);
|
|
const {value:branches}=await call('GET',prefix+'branches'+query);
|
|
await call('GET',prefix+'readme'+query);
|
|
const branch=branches.find(b=>b.name===fixture.branch);
|
|
assert(branch && typeof branch.commit==='string' && /^[a-f0-9]{40}$/.test(branch.commit), 'public fixture branch missing');
|
|
let expected='XXXX-1 World!\n';
|
|
if(appFlow) {
|
|
const source=await fetch('https://raw.githubusercontent.com/'+fixture.name+'/'+branch.commit+'/'+fixture.file,{signal:AbortSignal.timeout(15000)});
|
|
assert.equal(source.status,200,'public fixture source unavailable');
|
|
const original=await source.text();
|
|
assert(original.includes('MIT'),'fixture no longer exercises anonymization');
|
|
expected=original.replace(/\bMIT\b/gi,'XXXX-1');
|
|
}
|
|
await call('POST','/api/repo',{repoId,fullName:fixture.name,connection:appFlow?'github-app':'oauth',source:{provider:'github',branch:fixture.branch,commit:branch.commit},terms:[fixture.term],options:{expirationMode:'never',image:true,pdf:true,link:true}});
|
|
createdRepo=true;
|
|
if(process.argv.includes('--website')) db(`d.anonymizedrepositories.updateOne({repoId:'${repoId}'},{$set:{'options.page':true,'options.pageSource':{branch:'${fixture.branch}',path:'/'}}});`);
|
|
if(appFlow) db(`const row=d.anonymizedrepositories.findOne({repoId:'${repoId}'}); if(row?.githubAccess?.kind!=='github-app') throw Error('App binding missing');`);
|
|
let ready=false;
|
|
for(let i=0;i<30;i++) {
|
|
const {value:r}=await call('GET','/api/repo/'+repoId);
|
|
if(r.status==='ready') {ready=true;break;}
|
|
assert(r.status!=='error','repository processing failed');
|
|
await new Promise(r=>setTimeout(r,1000));
|
|
}
|
|
assert(ready,'repository did not become ready');
|
|
const {value:content}=await call('GET','/api/repo/'+repoId+'/file/'+fixture.file,undefined,200,'');
|
|
assert.equal(content,expected);
|
|
async function checkWebsite() {
|
|
if(!process.argv.includes('--website')) return;
|
|
const page=await call('GET','/w/'+repoId+'/'+fixture.file,undefined,200,'',{'Accept-Encoding':'gzip'});
|
|
assert.equal(page.value,expected,'website output differs from file');
|
|
assert(page.response.headers.get('content-security-policy').includes('sandbox'),'website sandbox missing');
|
|
}
|
|
await checkWebsite();
|
|
await call('GET','/api/repo/'+repoId+'/files',undefined,200,'');
|
|
const {value:zip}=await call('GET','/api/repo/'+repoId+'/zip',undefined,200,'',{},true);
|
|
function zipManifest(archive) {
|
|
const unzip=spawnSync('python3',['-c',`import io,sys,zipfile,json,base64,hashlib
|
|
v=json.load(sys.stdin)
|
|
with zipfile.ZipFile(io.BytesIO(base64.b64decode(v['zip']))) as z:
|
|
assert z.testzip() is None
|
|
files=[n for n in z.namelist() if not n.endswith('/')]
|
|
if v['single']: assert len(files)==1
|
|
matches=[n for n in files if n==v['file'] or n.endswith('/'+v['file'])]
|
|
assert len(matches)==1
|
|
assert z.read(matches[0])==v['expected'].encode('utf8')
|
|
print(json.dumps([[n, hashlib.sha256(z.read(n)).hexdigest()] for n in z.namelist()]))
|
|
`],{input:JSON.stringify({zip:archive.toString('base64'),file:fixture.file,expected,single:!appFlow}),timeout:10000});
|
|
assert(!unzip.error && unzip.status===0,'ZIP CRC, entries or anonymized bytes mismatch');
|
|
return JSON.parse(unzip.stdout.toString());
|
|
}
|
|
const originalManifest=zipManifest(zip);
|
|
report.zipManifestSHA256=createHash('sha256').update(JSON.stringify([...originalManifest].sort((a,b)=>a[0].localeCompare(b[0])))).digest('hex');
|
|
report.zipEntries=originalManifest.length;
|
|
report.zipSourceCommit=branch.commit;
|
|
report.exactZipContents='passed';
|
|
if(process.argv.includes('--repeat-content')) {
|
|
for(let i=0;i<2;i++) {
|
|
const again=await call('GET','/api/repo/'+repoId+'/file/'+fixture.file,undefined,200,'');
|
|
assert.equal(again.value,expected,'repeated file changed');
|
|
await checkWebsite();
|
|
const archive=await call('GET','/api/repo/'+repoId+'/zip',undefined,200,'',{},true);
|
|
// Timed streaming flushes can change deflate framing, but not archive contents.
|
|
assert.deepEqual(zipManifest(archive.value),originalManifest,'repeated ZIP contents changed');
|
|
}
|
|
report.repeatedContentChecks=3;
|
|
}
|
|
const {value:metrics}=await call('GET','/api/admin/performance');
|
|
assert.equal(metrics.available,true,'performance history unavailable');
|
|
assert(metrics.routes.some(row=>row.metric==='request' && row.count>0),'missing request metrics');
|
|
assert(metrics.instances.some(row=>row.runtime==='go' && row.memory?.rss>0),'missing Go process sample');
|
|
assert(metrics.runtimeSeries.length>0,'missing runtime history');
|
|
for(const stage of ['repository','authorization','worker_wait','anonymize']) {
|
|
assert(metrics.stages.some(row=>row.metric===stage && row.count>0),'missing stage '+stage);
|
|
}
|
|
report.performanceMonitoring={requests:true,processSamples:true,runtimeHistory:true,stages:['repository','authorization','worker_wait','anonymize'],upstreamObserved:metrics.stages.some(row=>row.metric==='upstream' && row.count>0),upstreamNote:'Shared-cache hits need no upstream read; provider timing is checked by the integration suite.'};
|
|
await call('GET','/api/user',undefined,401,randomBytes(32).toString('hex'));
|
|
report.result='passed';
|
|
})().catch(e=>{report.result='failed';report.failure=e.message;process.exitCode=1;}).finally(async()=>{
|
|
try {
|
|
if(createdRepo) {
|
|
await call('DELETE','/api/repo/'+repoId);
|
|
let removed=false;
|
|
for(let i=0;i<30;i++) {
|
|
const {value:r}=await call('GET','/api/repo/'+repoId);
|
|
if(r.status==='removed') {removed=true;break;}
|
|
await new Promise(r=>setTimeout(r,1000));
|
|
}
|
|
assert(removed,'test repository removal did not finish');
|
|
await call('GET','/api/repo/'+repoId+'/file/'+fixture.file,undefined,410,'');
|
|
report.testRepositoryRemoved=true;
|
|
}
|
|
} catch {report.testRepositoryRemoved=false;process.exitCode=1;}
|
|
if(bootstrapped) {
|
|
try {
|
|
db(`d.users.updateOne({'apiTokens._id':ObjectId('${id}')},{$pull:{apiTokens:{_id:ObjectId('${id}')}}});`);
|
|
await call('GET','/api/user',undefined,401);
|
|
report.temporaryKeyRevoked=true;
|
|
report.betaAccountAdmin=true;
|
|
} catch {report.temporaryKeyRevoked=false;process.exitCode=1;}
|
|
}
|
|
report.uniqueRequestIDsVerified=requestIDs.size;
|
|
if(process.exitCode) report.result='failed';
|
|
fs.writeFileSync(path.join(root,monitoringOnly?'backend/reports/shadow-monitoring-authenticated-deployment.json':appFlow?'backend/reports/shadow-app-authenticated-deployment.json':'backend/reports/shadow-authenticated-deployment.json'),JSON.stringify(report,null,2)+'\n');
|
|
console.log(JSON.stringify(report,null,2));
|
|
});
|