Skip to content

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';
2const test=require('node:test');
3const assert=require('node:assert/strict');
4const http=require('node:http');
5const {checkHttp,runProbe}=require('../src/probes');
7async 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));}
34test('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}));
41test('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}));
47test('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}));
51test('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}));
57test('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}));
63test('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}));