Files
jiang13-bbs/backend/tests/operations-api.mjs

73 lines
6.5 KiB
JavaScript

// Run only against an isolated development backend and matching Next.js frontend.
import assert from "node:assert/strict";
const base = process.env.OPS_TEST_BASE_URL;
if (process.env.OPS_TEST_CONFIRM !== "isolated" || !base || !/^http:\/\/(127\.0\.0\.1|localhost):3301$/.test(base)) throw Error("Requires isolated test backend on port 3301");
const jars = { admin: {}, user: {}, anon: {} };
async function request(path, method = "GET", body, who = "admin", csrf = true) {
const jar = jars[who]; const headers = { "Content-Type": "application/json", Cookie: Object.entries(jar).map(([k,v])=>k+"="+v).join("; ") };
if (csrf && jar.j13_csrf) headers["X-CSRF-Token"] = jar.j13_csrf;
const r = await fetch(base+path,{method,headers,...(body === undefined ? {} : {body:JSON.stringify(body)})});
for (const c of r.headers.getSetCookie()) { const pair=c.split(";")[0];const i=pair.indexOf("=");jar[pair.slice(0,i)]=pair.slice(i+1); }
const text=await r.text();let data;try{data=JSON.parse(text)}catch{data=text};return {status:r.status,data,retry:r.headers.get("retry-after")};
}
const admin = {username:"ops_admin",email:"admin@example.test",password:"Isolated-test-2026"};
const user = {username:"ops_member",email:"member@example.test",password:"Isolated-test-2026"};
for (const [who,body] of [["admin",admin],["user",user]]) {
let r=await request("/api/login","POST",body,who);
if (r.status!==200) r=await request("/api/register","POST",body,who);
assert.equal(r.status,200,JSON.stringify(r.data));
}
const priorMode=(await request("/api/admin/settings/modules/maintenance")).data;priorMode.data.mode="normal";assert.equal((await request("/api/admin/settings/modules/maintenance","PUT",priorMode)).status,200);
await request("/api/admin/settings","PUT",{post_cooldown_hours:0});
const riskAccount="risk-"+Date.now();
const modulePath = n => "/api/admin/settings/modules/"+n;
const read=async n=>(await request(modulePath(n))).data;
const save=async(n,s)=>(await request(modulePath(n),"PUT",{version:s.version,data:s.data}));
for (const path of [modulePath("mail"),modulePath("storage"),"/api/admin/diagnostics"]) {
assert.equal((await request(path,"GET",undefined,"anon")).status,401);
assert.equal((await request(path,"GET",undefined,"user")).status,403);
}
for (const path of [modulePath("mail")+"/records",modulePath("filter")+"/records"]) assert.equal((await request(path,"GET",undefined,"user")).status,403);
assert.equal((await request(modulePath("security"),"PUT",{},"user")).status,403);
assert.equal((await request("/api/admin/maintenance/actions","POST",{action:"scan"},"user")).status,403);
assert.equal((await request(modulePath("mail")+"/test","POST",{data:{},action:"connection"},"user")).status,403);
assert.equal((await request(modulePath("security"),"PUT",{},"admin",false)).status,403);
let sec=await read("security");const originalSec=structuredClone(sec.data);
sec.data.allow_register=false;assert.equal((await save("security",sec)).status,200);
assert.equal((await request("/api/register","POST",{username:"blocked",password:"Blocked-test-2026"},"anon")).status,403);
sec=await read("security");sec.data={...sec.data,allow_register:true,comment_interval:15,login_failures:3};
const concurrent=await Promise.all([save("security",sec),save("security",sec)]);
assert.deepEqual(concurrent.map(x=>x.status).sort(),[200,409]);
console.log("PASS permissions, CSRF, registration, concurrent version conflict");
for(let i=0;i<3;i++) assert.equal((await request("/api/login","POST",{username:riskAccount,password:"wrong"},"anon")).status,401);
let blocked=await request("/api/login","POST",{username:riskAccount,password:"wrong"},"anon");assert.equal(blocked.status,429);assert.ok(Number(blocked.retry)>0);
console.log("PASS direct API login protection and Retry-After");
let filter=await read("filter");const originalFilter=structuredClone(filter.data);
filter.data={enabled:true,rules:[{id:"api-test",word:"blockedphrase",scopes:["body","comment","username"],action:"block",enabled:true,exceptions:["notblockedphrase"],note:"isolated test"}]};
assert.equal((await save("filter",filter)).status,200);
const board=(await request("/api/boards")).data.boards[0].id;
const post=await request("/api/posts","POST",{board_id:board,title:"Operational validation",content:"Safe validation content"});
assert.equal(post.status,200,JSON.stringify(post.data));const postID=post.data.post.id;
let r=await request("/api/posts/"+postID,"PUT",{title:"Operational validation",content:"blockedphrase",board_id:board});assert.equal(r.status,400);
const tested=await request(modulePath("filter")+"/test","POST",{data:filter.data,scope:"body",text:"notblockedphrase and blockedphrase"});
assert.equal(tested.data.result,"block");assert.equal(tested.data.hits[0].excepted,true);
r=await request("/api/posts/"+postID+"/comments","POST",{content:"Validation comment"},"user");assert.equal(r.status,200,JSON.stringify(r.data));
r=await request("/api/posts/"+postID+"/comments","POST",{content:"Second comment"},"user");assert.equal(r.status,429);assert.ok(r.retry);
console.log("PASS create/edit filter, scoped exception, comment interval");
let m=await read("maintenance");m.data.mode="readonly";assert.equal((await save("maintenance",m)).status,200);
assert.equal((await request("/api/posts/"+postID+"/like","POST",{},"user")).status,503);
assert.equal((await request("/api/posts","GET",undefined,"anon")).status,200);
m=await read("maintenance");m.data.mode="paused";m.data.title="隔离测试维护中";assert.equal((await save("maintenance",m)).status,200);
r=await request("/api/posts","GET",undefined,"anon");assert.equal(r.status,503);assert.ok(r.retry);
assert.equal((await request(modulePath("maintenance"))).status,200);
assert.equal((await request("/api/login","POST",user,"anon")).status,200);
const html=await fetch("http://localhost:3000/");assert.equal(html.status,503);assert.ok(html.headers.get("retry-after"));assert.match(await html.text(),/隔离测试维护中/);
const robots=await fetch("http://localhost:3000/robots.txt");assert.equal(robots.status,200);
m=await read("maintenance");m.data.mode="normal";assert.equal((await save("maintenance",m)).status,200);
assert.equal((await fetch("http://localhost:3000/")).status,200);
filter=await read("filter");filter.data=originalFilter;assert.equal((await save("filter",filter)).status,200);
sec=await read("security");sec.data=originalSec;assert.equal((await save("security",sec)).status,200);
console.log("PASS read-only, paused API/page 503, robots, administrator recovery, restoration");
console.log("ALL API CHECKS PASSED; disposable browser login: ops_admin / Isolated-test-2026");