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