70 lines
8.8 KiB
JavaScript
70 lines
8.8 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=19000+Math.floor(Math.random()*10000);
|
|
const dataDir=fs.mkdtempSync(path.join(os.tmpdir(),'bend-field-shared-world-'));
|
|
const 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='';child.stderr.on('data',chunk=>stderr+=chunk);
|
|
const base=`http://127.0.0.1:${port}`;
|
|
const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
|
|
let aliceRealtime=null;
|
|
async function request(url,options={}){
|
|
const response=await fetch(base+url,options),body=await response.json();
|
|
return{response,body};
|
|
}
|
|
async function requestText(url){const response=await fetch(base+url),body=await response.text();return{response,body}}
|
|
function auth(session){return{authorization:`Bearer ${session.playerId}.${session.token}`,'content-type':'application/json'}}
|
|
function boardMeta(id,x,seed,puzzle){return{id,x,y:0,chunks:[[0,0]],level:1,targetLevel:1,seed,axis:'MIX',sealedSides:[],puzzle,rev:1,revAuthor:'client'}}
|
|
function solvedState(puzzle){return{paths:puzzle.solution.map(route=>({startGate:route.startGate,endGate:route.endGate,cells:route.cells.map(cell=>[...cell])})),solved:true,expanded:false,rev:2,revAuthor:'client'}}
|
|
(async()=>{
|
|
for(let i=0;i<60;i++){try{const {response}=await request('/api/cloud/status');if(response.ok)break}catch(_){}await sleep(50)}
|
|
const status=await request('/api/cloud/status');assert.equal(status.response.status,200);assert.equal(status.body.sharedWorld,true);
|
|
const page=await requestText('/');assert.equal(page.response.status,200);assert(page.body.indexOf('build-meta.js')<page.body.indexOf('puzzle-core.js')&&page.body.indexOf('app-logic.js')>page.body.indexOf('puzzle-core.js')&&page.body.indexOf('app-logic.js')<page.body.indexOf('app.js'));
|
|
const runtimeConfig=await requestText('/runtime-config.js');assert.equal(runtimeConfig.response.status,200);assert.match(runtimeConfig.body,/cloudApi:true/);
|
|
const logicAsset=await requestText('/app-logic.js');assert.equal(logicAsset.response.status,200);assert.match(logicAsset.body,/BendAppLogic/);
|
|
|
|
const alice=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Alice'})})).body;
|
|
const bob=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Bob'})})).body;
|
|
for(const session of[alice,bob]){assert.match(session.playerId,/^[a-f0-9]{16,64}$/);assert.match(session.token,/^[a-f0-9]{32,128}$/)}
|
|
assert.equal(alice.name,'Alice');assert.equal(bob.name,'Bob');
|
|
|
|
const starter=starterPuzzle(),b0=boardMeta('B0',0,123456,starter);
|
|
const bootstrap={baseRevision:0,global:{schema:31,appVersion:'47.70',generatorVersion:5,worldGeneration:'v47-field-reset-20260728-interaction-fix',nextId:1,score:999,cursorStyle:'do-not-share',cloudProfile:{token:'do-not-store'}},metas:[b0],states:[{id:'B0',value:{paths:[],solved:false,rev:1,revAuthor:'alice'}}],deleted:[]};
|
|
const pushed=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify(bootstrap)});assert.equal(pushed.response.status,200);assert.equal(pushed.body.revision,1);assert.deepEqual(pushed.body.clearEvents,[]);
|
|
|
|
const bobInitial=await request('/api/cloud/pull?since=0&eventsSince=0',{headers:auth(bob)});assert.equal(bobInitial.response.status,200);assert.equal(bobInitial.body.changed,true);assert.equal(bobInitial.body.fullSnapshot,true);assert.equal(bobInitial.body.page.metas.B0.seed,123456);assert.equal(bobInitial.body.page.states.B0.solved,false);assert(bobInitial.body.page.metas.B0.rev>1_000_000_000_000);assert(bobInitial.body.page.states.B0.rev>1_000_000_000_000);assert.equal(bobInitial.body.page.global.score,undefined);assert.equal(bobInitial.body.page.global.cursorStyle,undefined);assert.equal(bobInitial.body.page.global.cloudProfile,null);assert.equal(bobInitial.body.player.name,'Bob');
|
|
|
|
aliceRealtime=await connectRealtime(base,alice);aliceRealtime.send({type:'viewport',minX:-10,minY:-10,maxX:10,maxY:10});await aliceRealtime.waitFor('snapshot');aliceRealtime.send({type:'claim',requestId:'server-smoke-claim',boardId:'B0'});const claimResult=await aliceRealtime.waitFor(message=>message.type==='claim-result'&&message.requestId==='server-smoke-claim');assert.equal(claimResult.ok,true);
|
|
const clearPayload={baseRevision:1,global:{nextId:1,lastSolveAt:Date.now()},metas:[],states:[{id:'B0',value:solvedState(starter)}],deleted:[]};
|
|
const cleared=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify(clearPayload)});assert.equal(cleared.response.status,200);assert.equal(cleared.body.revision,2);assert.equal(cleared.body.clearEvents.length,1);assert.equal(cleared.body.clearEvents[0].playerName,'Alice');assert.equal(cleared.body.clearEvents[0].id,'B0');assert.equal(cleared.body.clearEvents[0].level,1);
|
|
|
|
const bobAfterClear=await request('/api/cloud/pull?since=1&eventsSince=0',{headers:auth(bob)});assert.equal(bobAfterClear.response.status,200);assert.equal(bobAfterClear.body.fullSnapshot,false);assert.equal(bobAfterClear.body.page.states.B0.solved,true);assert.equal(bobAfterClear.body.page.states.B0.solvedBy,'Alice');assert.equal(bobAfterClear.body.page.states.B0.solvedById,alice.playerId);assert(Number.isFinite(bobAfterClear.body.page.states.B0.solvedAt));assert.equal(bobAfterClear.body.clearEvents.length,1);assert.equal(bobAfterClear.body.clearEvents[0].playerName,'Alice');
|
|
|
|
const stale=await request('/api/cloud/push',{method:'POST',headers:auth(bob),body:JSON.stringify({...clearPayload,baseRevision:1})});assert.equal(stale.response.status,409);assert.equal(stale.body.revision,2);
|
|
const repeat=await request('/api/cloud/push',{method:'POST',headers:auth(bob),body:JSON.stringify({...clearPayload,baseRevision:2})});assert.equal(repeat.response.status,200);assert.equal(repeat.body.revision,3);assert.deepEqual(repeat.body.clearEvents,[]);
|
|
const preserved=await request('/api/cloud/pull?since=0&eventsSince=0',{headers:auth(bob)});assert.equal(preserved.body.page.states.B0.solvedBy,'Alice');
|
|
|
|
const renamed=await request('/api/cloud/profile',{method:'POST',headers:auth(bob),body:JSON.stringify({name:'Bob 2'})});assert.equal(renamed.response.status,200);assert.equal(renamed.body.name,'Bob 2');
|
|
const unchanged=await request('/api/cloud/pull?since=3&eventsSince=2',{headers:auth(bob)});assert.equal(unchanged.body.changed,false);assert.equal(unchanged.body.player.name,'Bob 2');
|
|
|
|
const b1=boardMeta('B1',1,987654,starter);
|
|
const generated=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:3,global:{nextId:2},metas:[b1],states:[{id:'B1',value:{paths:[],solved:false,rev:4,revAuthor:'alice'}}],deleted:[]})});assert.equal(generated.response.status,200);assert.equal(generated.body.revision,4);
|
|
const bobGenerated=await request('/api/cloud/pull?since=3&eventsSince=2',{headers:auth(bob)});assert.equal(bobGenerated.response.status,200);assert.equal(bobGenerated.body.page.metas.B1.seed,987654);assert.equal(bobGenerated.body.page.states.B1.solved,false);
|
|
|
|
const invalid=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:4,global:{},metas:[{...b1,id:'B2',x:2,chunks:[]}],states:[],deleted:[]})});assert.equal(invalid.response.status,400);
|
|
const overlap=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:4,global:{},metas:[boardMeta('B2',1,222,starter)],states:[],deleted:[]})});assert.equal(overlap.response.status,400);
|
|
|
|
|
|
const aliceRecord=JSON.parse(fs.readFileSync(path.join(dataDir,`${alice.playerId}.json`),'utf8'));assert.equal(aliceRecord.name,'Alice');assert.notEqual(aliceRecord.tokenHash,alice.token);assert.equal(aliceRecord.boardVersions,undefined);assert.equal(aliceRecord.global,undefined);
|
|
const world=JSON.parse(fs.readFileSync(path.join(dataDir,'shared-world.json'),'utf8'));assert.equal(world.revision,4);assert.equal(world.global.score,undefined);assert.equal(world.global.cursorStyle,undefined);assert.equal(world.boardVersions.B0,3);assert.equal(world.boardVersions.B1,4);assert.equal(world.clearEvents[0].playerName,'Alice');
|
|
const b0Shard=JSON.parse(fs.readFileSync(path.join(dataDir,'shared-world.boards','B0.3.json'),'utf8'));assert.equal(b0Shard.state.solvedBy,'Alice');assert.equal(b0Shard.state.paths.length,starter.solution.length);
|
|
console.log('BEND FIELD shared-world phase 2 server smoke test passed');
|
|
})().catch(error=>{console.error(error);if(stderr)console.error(stderr);process.exitCode=1}).finally(()=>{aliceRealtime?.close();child.kill('SIGTERM');fs.rmSync(dataDir,{recursive:true,force:true})});
|