bend_puzzle/test/security-authority-smoke-test.js
2026-07-31 12:54:46 +09:00

15 lines
4.1 KiB
JavaScript

'use strict';
const {spawn}=require('child_process');const fs=require('fs');const os=require('os');const path=require('path');const assert=require('assert/strict');
const {root,starterPuzzle}=require('./helpers/app-source');const {connectRealtime}=require('./helpers/realtime-client');
const port=32000+Math.floor(Math.random()*2000),dataDir=fs.mkdtempSync(path.join(os.tmpdir(),'bend-auth-')),child=spawn(process.execPath,[path.join(root,'server.js')],{env:{...process.env,PORT:String(port),HOST:'127.0.0.1',BEND_FIELD_DATA_DIR:dataDir},stdio:['ignore','pipe','pipe']});let stderr='',ws;child.stderr.on('data',c=>stderr+=c);const base=`http://127.0.0.1:${port}`,sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function req(url,opt={}){const response=await fetch(base+url,opt);return{response,body:await response.json()}}function auth(s){return{authorization:`Bearer ${s.playerId}.${s.token}`,'content-type':'application/json'}}
function meta(id,x,seed,p){return{id,x,y:0,chunks:[[0,0]],level:1,targetLevel:1,seed,axis:'MIX',sealedSides:[],puzzle:p,rev:1,revAuthor:'x'}}
(async()=>{for(let i=0;i<100;i++){try{if((await req('/api/cloud/status')).response.ok)break}catch{}await sleep(30)}const alice=(await req('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Alice'})})).body,p=starterPuzzle(),route=[[0,1],[0,0],[1,0],[2,0],[3,0],[4,0],[4,1],[3,1],[2,1],[1,1],[1,2],[0,2],[0,3],[0,4],[1,4],[1,3],[2,3],[2,4],[3,4],[4,4],[4,3],[3,3],[3,2],[4,2]];p.g=[[0,1,'N'],[4,2,'S']];p.n=[[0,0,10]];p.valid=route.map(cell=>[...cell]);p.obstacles=[[2,2]];p.solution=[{startGate:0,endGate:1,cells:route.map(cell=>[...cell])}];p.specialCells={crossings:[],warps:[],locks:[],internalGates:[]};const b0=meta('B0',0,15,p);
let r=await req('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:0,global:{nextId:1},metas:[b0],states:[{id:'B0',value:{paths:[],solved:false}}]})});assert.equal(r.response.status,200);ws=await connectRealtime(base,alice);ws.send({type:'viewport',minX:-5,minY:-5,maxX:5,maxY:5});await ws.waitFor('snapshot');ws.send({type:'claim',requestId:'c',boardId:'B0'});assert.equal((await ws.waitFor(m=>m.type==='claim-result'&&m.requestId==='c')).ok,true);
const solved={paths:p.solution.map(q=>({startGate:q.startGate,endGate:q.endGate,cells:q.cells})),solved:true,scoreAwarded:Number.MAX_SAFE_INTEGER,store:{pathIndex:0,cellIndex:0,itemIds:['level-min-10'],priceCoefficient:0,purchases:[]}};
r=await req('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:1,global:{nextId:1},metas:[],states:[{id:'B0',value:solved}]})});assert.equal(r.response.status,200);const pulled=await req('/api/cloud/pull?since=0',{headers:auth(alice)}),state=pulled.body.page.states.B0;assert(state.scoreAwarded>0&&state.scoreAwarded<100000,'server must replace forged reward');assert(state.store&&state.store.priceCoefficient>=.8&&state.store.priceCoefficient<=1.2,'server must replace forged store pricing');assert.equal(state.store.itemIds.length,13);
const econ=(await req('/api/player/state',{headers:auth(alice)})).body.player;assert.equal(econ.earnedScore,state.scoreAwarded);assert.equal(econ.availableScore,state.scoreAwarded);
const forged=meta('B1',999999,123,p);r=await req('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:2,global:{nextId:2},metas:[forged],states:[{id:'B1',value:{paths:[],solved:false}}]})});assert.notEqual(r.response.status,200,'non-adjacent forged board must be rejected');
const forgedDifficulty=meta('B1',1,123,p);forgedDifficulty.level=10;r=await req('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:2,global:{nextId:2},metas:[forgedDifficulty],states:[{id:'B1',value:{paths:[],solved:false}}]})});assert.equal(r.response.status,400,'client-authored difficulty must be rejected in favor of server-derived puzzle facts');
console.log('Server authority security smoke test passed');
})().catch(e=>{console.error(e);if(stderr)console.error(stderr);process.exitCode=1}).finally(()=>{ws?.close();child.kill('SIGTERM');fs.rmSync(dataDir,{recursive:true,force:true})});