Repository files, collaboration, releases and project activity.
archforge-external-status / tests/redirect-monitor.test.jsmain
▤ tests/redirect-monitor.test.js3869 bytes
69 lines · Select a line number for a permalink
1
'use strict';2
const test=require('node:test');3
const assert=require('node:assert/strict');4
const http=require('node:http');5
const {checkHttp,runProbe}=require('../src/probes');7
async function withServer(fn){8
const s=http.createServer((req,res)=>{9
const route=req.url.split('?')[0];10
const redirect=(to,code=302)=>{res.writeHead(code,{location:to});res.end();};11
if(route==='/')return redirect('/login');12
if(route==='/login'){res.writeHead(200,{'content-type':'text/html'});res.end('<html>Login</html>');return;}13
if(route==='/healthy'){res.writeHead(200,{'content-type':'text/html'});res.end('<html>Site</html>');return;}14
if(route==='/one')return redirect('/two',301);15
if(route==='/two')return redirect('/healthy',308);16
if(route==='/offsite')return redirect('https://untrusted.example/');17
if(route==='/downgrade')return redirect('http://evil.invalid/');18
if(route==='/credentials')return redirect('http://user:password@127.0.0.1/');19
if(route==='/bad-scheme')return redirect('file:///etc/passwd');20
if(route==='/self')return redirect('/self');21
if(route==='/no-location'){res.writeHead(302);res.end();return;}22
if(route==='/chain1')return redirect('/chain2');23
if(route==='/chain2')return redirect('/chain3');24
if(route==='/chain3')return redirect('/chain4');25
if(route==='/chain4')return redirect('/healthy');26
if(route==='/servererror'){res.writeHead(500);res.end('Internal Server Error');return;}27
if(route==='/git'){res.writeHead(302,{location:'/login'});res.end();return;}28
res.writeHead(404);res.end();29
});30
await new Promise(r=>s.listen(0,'127.0.0.1',r));31
try{return await fn('http://127.0.0.1:'+s.address().port);}finally{await new Promise(r=>s.close(r));}32
}34
test('real-world HTTP 302 to same-origin login: website reachable, clearly marked reachability-only',async()=>withServer(async base=>{35
const r=await checkHttp(base+'/',1200,{website:true});36
assert.equal(r.success,true);37
assert.match(r.detail,/HTTP 200 via 302/);38
assert.match(r.detail,/reachability only/);39
assert.equal((await runProbe('website',{website:base+'/',timeout:1200})).success,true);40
}));41
test('API and ArchForge HTTPS never follow 302 to a login page',async()=>withServer(async base=>{42
const api=await checkHttp(base+'/',1200);43
const git=await checkHttp(base+'/git',1200,{git:true});44
assert.equal(api.success,false);assert.match(api.detail,/redirect rejected/);45
assert.equal(git.success,false);assert.match(git.detail,/redirect rejected/);46
}));47
test('safe 301 + 308 hops and actual website 200 count as reachable',async()=>withServer(async base=>{48
const r=await checkHttp(base+'/one',1200,{website:true});49
assert.equal(r.success,true);assert.match(r.detail,/2 same-origin hops/);50
}));51
test('external origins, protocol change, login credential injection and file scheme all rejected',async()=>withServer(async base=>{52
for(const route of ['/offsite','/downgrade','/credentials','/bad-scheme']){53
const r=await checkHttp(base+route,1200,{website:true});54
assert.equal(r.success,false,route);assert.match(r.detail,/unsafe redirect/,route);55
}56
}));57
test('redirect loops, 4th redirects, missing Location rejected',async()=>withServer(async base=>{58
for(const [route,error] of [['/self',/redirect loop/],['/chain1',/redirect limit/],['/no-location',/missing Location/]]){59
const r=await checkHttp(base+route,1200,{website:true});60
assert.equal(r.success,false,route);assert.match(r.detail,error,route);61
}62
}));63
test('HTTP 500 or 404 never reported as healthy',async()=>withServer(async base=>{64
const e=await checkHttp(base+'/servererror',1200,{website:true});65
const missing=await checkHttp(base+'/missing',1200,{website:true});66
assert.equal(e.success,false);assert.match(e.detail,/HTTP 500/);67
assert.equal(missing.success,false);assert.match(missing.detail,/HTTP 404/);68
}));