Files
probably-rendering-one-more…/apps/waterfall-wiki/tests/auth.test.ts
T
codex-bot 0e47bae606
waterfall-wiki CI and deployment / verify-and-deploy (pull_request) Failing after 3m28s
Merge remote-tracking branch 'origin/main' into codex/guidance-publishing
# Conflicts:
#	apps/waterfall-wiki/ACCOUNT.md
#	apps/waterfall-wiki/tests/auth.test.ts
2026-10-29 16:55:12 +08:00

57 lines
4.6 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {base32,totp,matchingStep,newAuthConfig,signSession,validSession,sessionCookie,safeReturn,authConfig,isLoginRedirect} from '../src/lib/auth';
import {proxy} from '../src/proxy';
import {NextRequest} from 'next/server';
import {mkdtempSync,writeFileSync,rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
test('TOTP matches RFC 6238 SHA-1 test vectors',()=>{
const secret=base32(Buffer.from('12345678901234567890'));
for(const [time,expected] of [[59,'94287082'],[1111111109,'07081804'],[1111111111,'14050471'],[1234567890,'89005924'],[2000000000,'69279037'],[20000000000,'65353130']] as const) assert.equal(totp(secret,Math.floor(time/30),8),expected);
assert.equal(matchingStep(secret,totp(secret,100),3000000),100);
assert.equal(matchingStep(secret,totp(secret,99),3000000),99);
assert.equal(matchingStep(secret,totp(secret,98),3000000),null);
assert.equal(matchingStep(secret,'123',3000000),null);
});
test('JWT rejects tampering, expiry, wrong keys and malformed values',()=>{
const config=newAuthConfig(),token=signSession(config,1000);
assert.equal(validSession(token,config,1000),true);
assert.equal(validSession(token,config,1000+604800),false);
assert.equal(validSession(token,config,900),false);
assert.equal(validSession(token,newAuthConfig(),1000),false);
assert.equal(validSession(token+'x',config,1000),false);
assert.equal(validSession('eyJhbGciOiJub25lIn0.e30.',config,1000),false);
assert.equal(validSession(undefined,config),false);
});
test('cookies are HTTP-only and secure by default; redirects stay local',()=>{
assert.match(sessionCookie('test'),/HttpOnly; SameSite=Lax; Max-Age=604800; Secure$/);
assert.match(sessionCookie('',true),/Max-Age=0/);
for(const path of ['https://evil.test','//evil.test','/\\evil.test','/login?next=/','/\nLocation: bad']) assert.equal(safeReturn(path),'/');
assert.equal(safeReturn('/explore/123?q=test'),'/explore/123?q=test');
});
test('proxy protects APIs, data routes, static content and Studio; fails closed',()=>{
const temp=mkdtempSync(join(tmpdir(),'prompt-auth-')); const old=process.env.AUTH_CONFIG_FILE;
try{
const config=newAuthConfig(); process.env.AUTH_CONFIG_FILE=join(temp,'config.json'); writeFileSync(process.env.AUTH_CONFIG_FILE,JSON.stringify(config));
for(const path of ['/library/import','/skills/example.png','/_next/data/build/index.json','/studio']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,307,path);
for(const path of ['/api/upload','/studio/api/export','/api/feed']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,401,path);
for(const path of ['/','/write','/documents','/api/documents','/api/documents/preview','/guide/welcome','/protocol','/api/guides/welcome','/account','/api/account/session','/login','/brand-mark.svg','/_next/static/chunk.js','/api/health','/api/ready']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,200,path);
const headers={cookie:'prompt_session='+signSession(config),host:'wiki.test'};
assert.equal(proxy(new NextRequest('http://wiki.test/api/feed',{headers})).status,200);
assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{...headers,origin:'http://evil.test'}})).status,403);
assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{...headers,origin:'http://wiki.test'}})).status,200);
assert.equal(proxy(new NextRequest('http://wiki.test/api/posts',{method:'POST',headers:{cookie:'wiki_account='+ 'a'.repeat(64)}})).status,401);
assert.equal(proxy(new NextRequest('http://wiki.test/guide/welcome',{method:'POST'})).status,401);
writeFileSync(process.env.AUTH_CONFIG_FILE,'{}'); assert.throws(()=>authConfig()); assert.equal(proxy(new NextRequest('http://wiki.test/api/posts')).status,503);
delete process.env.AUTH_CONFIG_FILE;
assert.equal(proxy(new NextRequest('http://wiki.test/api/posts')).status,503,'Missing admin configuration fails closed');
}finally{if(old===undefined)delete process.env.AUTH_CONFIG_FILE;else process.env.AUTH_CONFIG_FILE=old;rmSync(temp,{recursive:true,force:true});}
});
test('login redirects recognize browser pages and Next.js data destinations',()=>{
const base='http://wiki.test';
for(const target of ['/login?next=%2F','http://wiki.test/login','/_next/data/build-123/login.json?next=%2F']) assert.equal(isLoginRedirect(target,base),true,target);
for(const target of [null,'','https://evil.test/login','//evil.test/login','/api/login.json','/_next/data/build-123/index.json']) assert.equal(isLoginRedirect(target,base),false,String(target));
});